{"record":{"id":"92dd66ab913eca72","repo":"remix-run/react-router","slug":"invalid-redirect-location-92dd66","errorCode":null,"errorMessage":"Invalid redirect location","messagePattern":"Invalid redirect location","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/react-router/lib/hooks.tsx","lineNumber":1177,"sourceCode":"    error &&\n    \"digest\" in error &&\n    typeof error.digest === \"string\"\n  ) {\n    let redirect = decodeRedirectErrorDigest(error.digest);\n    if (redirect) {\n      let existingRedirect = errorRedirectHandledMap.get(error);\n      if (existingRedirect) throw existingRedirect;\n\n      let parsed = parseToInfo(redirect.location, basename);\n      let target = parsed.absoluteURL || parsed.to;\n      validateNavigationTarget(\n        redirect.location,\n        target,\n        getNavigatorCurrentUrl(navigator),\n        \"allow-explicit\",\n      );\n      if (hasInvalidProtocol(target)) {\n        throw new Error(\"Invalid redirect location\");\n      }\n\n      if (isBrowser && !errorRedirectHandledMap.get(error)) {\n        if (parsed.isExternal || redirect.reloadDocument) {\n          window.location.href = target;\n        } else {\n          const redirectPromise: Promise<void> = Promise.resolve().then(() =>\n            window.__reactRouterDataRouter!.navigate(parsed.to, {\n              replace: redirect.replace,\n            }),\n          );\n          errorRedirectHandledMap.set(error, redirectPromise);\n          throw redirectPromise;\n        }\n      }\n\n      return <meta httpEquiv=\"refresh\" content={`0;url=${target}`} />;\n    }","sourceCodeStart":1159,"sourceCodeEnd":1195,"githubUrl":"https://github.com/remix-run/react-router/blob/7aea711dd1ae2bc5a076d13ff17291829690fa74/packages/react-router/lib/hooks.tsx#L1159-L1195","documentation":"When a redirect thrown on the server crosses the RSC/error-digest boundary into client render, React Router decodes the digest, parses the redirect location, and validates its protocol against a blocklist (`javascript:`, `data:`, `blob:`, `file:`, `chrome-extension:`, etc. via `hasInvalidProtocol`). Throwing here is a deliberate security guard: silently honoring such a location would allow `javascript:` XSS through server-controlled redirects.","triggerScenarios":"`redirect(\"javascript:alert(1)\")` or a redirect to a `data:` URL thrown from a loader/action/middleware; a redirect location built from user input (query param `?next=`) that contains an unsafe scheme; an attacker-supplied URL stored in a DB and used as a redirect target.","commonSituations":"Open-redirect style flows (`redirect(searchParams.get(\"next\"))`) where the value is attacker-controlled; importing legacy URLs that embed `javascript:` links; penetration tests flagging redirect handling.","solutions":["Validate redirect targets against an allowlist before calling `redirect()`: only allow relative paths or `http:`/`https:` URLs.","Prefer relative locations (`redirect(url.pathname + url.search)`) after parsing with `new URL(value, request.url)`.","Reject or sanitize user-provided redirect inputs at the trust boundary (loader args, form fields).","Audit any DB-stored or config-driven redirect destinations for unsafe schemes."],"exampleFix":"// before\nexport async function loader({ request }: LoaderFunctionArgs) {\n  const next = new URL(request.url).searchParams.get(\"next\") ?? \"/\";\n  return redirect(next);\n}\n\n// after\nexport async function loader({ request }: LoaderFunctionArgs) {\n  const raw = new URL(request.url).searchParams.get(\"next\") ?? \"/\";\n  const url = new URL(raw, request.url);\n  if (url.origin !== new URL(request.url).origin) throw redirect(\"/\");\n  return redirect(url.pathname + url.search);\n}","handlingStrategy":"validation","validationCode":"function safeRedirectTarget(raw: string, request: Request): string | null {\n  try {\n    const url = new URL(raw, request.url);\n    return url.protocol === \"http:\" || url.protocol === \"https:\"\n      ? url.pathname + url.search\n      : null;\n  } catch {\n    return null;\n  }\n}","typeGuard":"const isSafeRedirect = (raw: string): boolean => {\n  try {\n    const p = new URL(raw, \"http://x\").protocol;\n    return p === \"http:\" || p === \"https:\";\n  } catch {\n    return false;\n  }\n};","tryCatchPattern":null,"preventionTips":["Allowlist protocols on every redirect target derived from user input.","Prefer relative redirects (pathname + search) over absolute URLs.","Add a test that asserts redirects reject javascript:/data: targets."],"tags":["redirect","security","protocol-validation","xss"],"backgroundTag":"unsafe-redirect-blocked","analyzedSha":"7aea711dd1ae2bc5a076d13ff17291829690fa74","analyzedAt":"2026-08-28T11:01:48.259Z","contentChangedAt":"2026-08-28T11:01:48.259Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}