{"record":{"id":"e97971758cee6146","repo":"remix-run/react-router","slug":"invalid-redirect-location-e97971","errorCode":null,"errorMessage":"Invalid redirect location","messagePattern":"Invalid redirect location","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/react-router/lib/rsc/server.ssr.tsx","lineNumber":208,"sourceCode":"    }) as DecodedPayload;\n  };\n\n  let renderRedirect: { status: number; location: string } | undefined;\n  let renderError: unknown;\n\n  try {\n    if (!detectRedirectResponse.body) {\n      throw new Error(\"Failed to clone server response\");\n    }\n    const payload = (await createFromReadableStream(\n      detectRedirectResponse.body,\n    )) as RSCPayload;\n    if (\n      serverResponse.status === SINGLE_FETCH_REDIRECT_STATUS &&\n      payload.type === \"redirect\"\n    ) {\n      if (hasInvalidProtocol(payload.location)) {\n        throw new Error(\"Invalid redirect location\");\n      }\n\n      const headers = new Headers(serverResponse.headers);\n      headers.delete(\"Content-Encoding\");\n      headers.delete(\"Content-Length\");\n      headers.delete(\"Content-Type\");\n      headers.delete(\"X-Remix-Response\");\n      headers.set(\"Location\", payload.location);\n\n      return new Response(serverResponseB?.body || \"\", {\n        headers,\n        status: payload.status,\n        statusText: serverResponse.statusText,\n      });\n    }\n\n    let reactHeaders = new Headers();\n    let status = serverResponse.status;","sourceCodeStart":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/remix-run/react-router/blob/7aea711dd1ae2bc5a076d13ff17291829690fa74/packages/react-router/lib/rsc/server.ssr.tsx#L190-L226","documentation":"Thrown by the RSC SSR server when it decodes a single-fetch redirect payload (server response with status 202, the SINGLE_FETCH_REDIRECT_STATUS) whose `location` uses a blocked protocol. `hasInvalidProtocol` matches Chrome's URL blocklist (`javascript:`, `data:`, `file:`, `blob:`, `about:`, `chrome:`, `chrome-untrusted:`, `content:`, `devtools:`, `filesystem:`), so the server refuses to copy that location into the `Location` header of the response it returns. This is a security guard against open-redirect/XSS via attacker-influenced redirect targets, not a generic parse failure.","triggerScenarios":"A loader or action in an RSC (unstable React Server Components) app returns or throws `redirect()` (or a hand-built Response with a Location header) whose target parses to a blocked protocol; during SSR the framework clones the server response, `createFromReadableStream` decodes the RSC payload, sees `payload.type === \"redirect\"` with status 202, and the protocol check on `payload.location` fails.","commonSituations":"Passing a user-controlled query parameter (e.g. `?redirectTo=`) straight into `redirect()` and a malicious client sends `javascript:alert(1)` or `data:text/html,...`; a CMS or upstream API returning a `file:`/`data:` URL that the loader forwards; penetration-test payloads hitting SSO/login `returnTo` flows.","solutions":["Find the loader/action that produced the redirect and stop feeding it untrusted locations: allow-list same-origin relative paths (must start with a single `/`, not `//`).","If the location legitimately comes from an external source, resolve and validate it against your own origin before calling `redirect()`.","Keep the exception path deliberate: catch this error in your request handler and respond 400 instead of letting the SSR request 500.","Add a regression test that sends `?redirectTo=javascript:...` and asserts a safe response."],"exampleFix":"// before\nexport async function loader({ request }: LoaderFunctionArgs) {\n  let to = new URL(request.url).searchParams.get(\"redirectTo\") ?? \"/\";\n  return redirect(to); // javascript: payload reaches the RSC SSR redirect path\n}\n\n// after\nconst SAFE = /^\\/[^\\/]/; // root-relative, not protocol-relative\nexport async function loader({ request }: LoaderFunctionArgs) {\n  let to = new URL(request.url).searchParams.get(\"redirectTo\") ?? \"/\";\n  return redirect(SAFE.test(to) ? to : \"/\");\n}","handlingStrategy":"validation","validationCode":"const BLOCKED_PROTOCOLS = [\n  \"about:\", \"blob:\", \"chrome:\", \"chrome-untrusted:\", \"content:\",\n  \"data:\", \"devtools:\", \"file:\", \"filesystem:\", \"javascript:\",\n];\nfunction isSafeRedirectTarget(location: string): boolean {\n  try {\n    return !BLOCKED_PROTOCOLS.includes(new URL(location).protocol);\n  } catch {\n    return true; // relative paths don't parse; they're fine\n  }\n}\n// before returning a redirect from a loader/action:\nif (!isSafeRedirectTarget(to)) to = \"/\";","typeGuard":"function isRootRelativePath(loc: string): loc is `/${string}` {\n  return /^\\/[^\\/]/.test(loc); // starts with '/', not '//'\n}","tryCatchPattern":"try {\n  return await handleDocumentRequest(request);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Invalid redirect location\") {\n    return new Response(\"Bad redirect target\", { status: 400 });\n  }\n  throw e;\n}","preventionTips":["Always normalize redirect targets to root-relative paths (single leading slash, never '//') before calling redirect().","Never pass query parameters, DB fields, or upstream API values directly as redirect locations.","For absolute redirects, verify new URL(target).origin equals the request origin.","Add tests that submit javascript:, data:, and //evil.com targets to every redirecting route."],"tags":["rsc","ssr","redirect","security","open-redirect"],"backgroundTag":"unsafe-redirect-location","analyzedSha":"7aea711dd1ae2bc5a076d13ff17291829690fa74","analyzedAt":"2026-08-18T18:04:14.938Z","contentChangedAt":"2026-08-18T18:04:14.938Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}