CVE-2025-43864
Description
React Router is a router for React. Starting in version 7.2.0 and prior to version 7.5.2, it is possible to force an application to switch to SPA mode by adding a header to the request. If the application uses SSR and is forced to switch to SPA, this causes an error that completely corrupts the page. If a cache system is in place, this allows the response containing the error to be cached, resulting in a cache poisoning that strongly impacts the availability of the application. This issue has been patched in version 7.5.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
react-routernpm | >= 7.2.0, < 7.5.2 | 7.5.2 |
Patches
25819e0c45df6c84302972a15Adjust approach for prerendering/SPA mode via headers (#13453)
7 files changed · +95 −31
.changeset/stale-bats-swim.md+6 −0 added@@ -0,0 +1,6 @@ +--- +"@react-router/dev": patch +"react-router": patch +--- + +Adjust approach for Prerendering/SPA Mode via headers
integration/vite-prerender-test.ts+17 −5 modified@@ -602,7 +602,7 @@ test.describe("Prerendering", () => { "app/routes/about.tsx": js` import { useLoaderData } from 'react-router'; export function loader({ request }) { - return "ABOUT-" + request.headers.has('X-React-Router-Prerender'); + return "ABOUT-" + Boolean(process.env.IS_RR_BUILD_REQUEST); } export default function Comp() { @@ -613,7 +613,7 @@ test.describe("Prerendering", () => { "app/routes/not-prerendered.tsx": js` import { useLoaderData } from 'react-router'; export function loader({ request }) { - return "NOT-PRERENDERED-" + request.headers.has('X-React-Router-Prerender'); + return "NOT-PRERENDERED-" + Boolean(process.env.IS_RR_BUILD_REQUEST); } export default function Comp() { @@ -659,7 +659,7 @@ test.describe("Prerendering", () => { import { useLoaderData } from 'react-router'; export function loader({ request }) { return { - prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no', + prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no", // 24999 characters data: new Array(5000).fill('test').join('-'), }; @@ -712,7 +712,7 @@ test.describe("Prerendering", () => { import { useLoaderData } from 'react-router'; export function loader({ request }) { return { - prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no', + prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no", data: "한글 데이터 - UTF-8 문자", }; } @@ -732,7 +732,7 @@ test.describe("Prerendering", () => { import { useLoaderData } from 'react-router'; export function loader({ request }) { return { - prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no', + prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no", data: "非プリレンダリングデータ - UTF-8文字", }; } @@ -837,6 +837,18 @@ test.describe("Prerendering", () => { await page.waitForSelector("[data-mounted]"); expect(await app.getHtml()).toMatch("Index: INDEX"); }); + + test("Ignores build-time headers at runtime", async () => { + fixture = await createFixture({ files }); + let res = await fixture.requestSingleFetchData("/_root.data", { + headers: { + "X-React-Router-Prerender-Data": encodeURI( + '[{"_1":2},"routes/_index",{"_3":4},"data","Hello World!"]' + ), + }, + }); + expect((res.data as any)["routes/_index"].data).toBe("Index Loader Data"); + }); }); test.describe("ssr: false", () => {
integration/vite-spa-mode-test.ts+31 −0 modified@@ -234,6 +234,37 @@ test.describe("SPA Mode", () => { expect(await res.text()).toMatch(/^<!DOCTYPE html><html lang="en">/); }); + test("Ignores build-time headers at runtime", async () => { + let fixture = await createFixture({ + files: { + "react-router.config.ts": reactRouterConfig({ + splitRouteModules, + }), + "app/root.tsx": js` + import { Outlet, Scripts } from "react-router"; + + export default function Root() { + return ( + <html lang="en"> + <head></head> + <body> + <h1 data-root>Root</h1> + <Scripts /> + </body> + </html> + ); + } + `, + }, + }); + let res = await fixture.requestDocument("/", { + headers: { "X-React-Router-SPA-Mode": "yes" }, + }); + let html = await res.text(); + expect(html).toMatch('"isSpaMode":false'); + expect(html).toMatch('<h1 data-root="true">Root</h1>'); + }); + test("works when combined with a basename", async ({ page }) => { fixture = await createFixture({ spaMode: true,
packages/react-router-dev/vite/plugin.ts+11 −16 modified@@ -1724,6 +1724,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { ); } + // Set an environment variable we can look for in the handler to + // enable some build-time-only logic + process.env.IS_RR_BUILD_REQUEST = "yes"; + if (isPrerenderingEnabled(ctx.reactRouterConfig)) { // If we have prerender routes, that takes precedence over SPA mode // which is ssr:false and only the root route being rendered @@ -2623,11 +2627,6 @@ async function handlePrerender( } let buildRoutes = createPrerenderRoutes(build.routes); - let headers = { - // Header that can be used in the loader to know if you're running at - // build time or runtime - "X-React-Router-Prerender": "yes", - }; for (let path of build.prerender) { // Ensure we have a leading slash for matching let matches = matchRoutes(buildRoutes, `/${path}/`.replace(/^\/\/+/, "/")); @@ -2655,17 +2654,15 @@ async function handlePrerender( [leafRoute.id], clientBuildDirectory, reactRouterConfig, - viteConfig, - { headers } + viteConfig ); // Prerender a raw file for external consumption await prerenderResourceRoute( handler, path, clientBuildDirectory, reactRouterConfig, - viteConfig, - { headers } + viteConfig ); } else { viteConfig.logger.warn( @@ -2684,8 +2681,7 @@ async function handlePrerender( null, clientBuildDirectory, reactRouterConfig, - viteConfig, - { headers } + viteConfig ); } @@ -2698,11 +2694,10 @@ async function handlePrerender( data ? { headers: { - ...headers, "X-React-Router-Prerender-Data": encodeURI(data), }, } - : { headers } + : undefined ); } } @@ -2746,7 +2741,7 @@ async function prerenderData( clientBuildDirectory: string, reactRouterConfig: ResolvedReactRouterConfig, viteConfig: Vite.ResolvedConfig, - requestInit: RequestInit + requestInit?: RequestInit ) { let normalizedPath = `${reactRouterConfig.basename}${ prerenderPath === "/" @@ -2789,7 +2784,7 @@ async function prerenderRoute( clientBuildDirectory: string, reactRouterConfig: ResolvedReactRouterConfig, viteConfig: Vite.ResolvedConfig, - requestInit: RequestInit + requestInit?: RequestInit ) { let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace( /\/\/+/g, @@ -2845,7 +2840,7 @@ async function prerenderResourceRoute( clientBuildDirectory: string, reactRouterConfig: ResolvedReactRouterConfig, viteConfig: Vite.ResolvedConfig, - requestInit: RequestInit + requestInit?: RequestInit ) { let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/` .replace(/\/\/+/g, "/")
packages/react-router/lib/server-runtime/dev.ts+12 −0 modified@@ -14,3 +14,15 @@ export function getDevServerHooks(): DevServerHooks | undefined { // @ts-expect-error return globalThis[globalDevServerHooksKey]; } + +// Guarded access to build-time-only headers +export function getBuildTimeHeader(request: Request, headerName: string) { + if (typeof process !== "undefined") { + try { + if (process.env?.IS_RR_BUILD_REQUEST === "yes") { + return request.headers.get(headerName); + } + } catch (e) {} + } + return null; +}
packages/react-router/lib/server-runtime/routes.ts+6 −4 modified@@ -19,6 +19,7 @@ import { } from "../dom/ssr/single-fetch"; import invariant from "./invariant"; import type { ServerRouteModule } from "../dom/ssr/routeModules"; +import { getBuildTimeHeader } from "./dev"; export type ServerRouteManifest = RouteManifest<Omit<ServerRoute, "children">>; @@ -86,10 +87,11 @@ export function createStaticHandlerDataRoutes( ? async (args: RRLoaderFunctionArgs) => { // If we're prerendering, use the data passed in from prerendering // the .data route so we don't call loaders twice - if (args.request.headers.has("X-React-Router-Prerender-Data")) { - const preRenderedData = args.request.headers.get( - "X-React-Router-Prerender-Data" - ); + let preRenderedData = getBuildTimeHeader( + args.request, + "X-React-Router-Prerender-Data" + ); + if (preRenderedData != null) { let encoded = preRenderedData ? decodeURI(preRenderedData) : preRenderedData;
packages/react-router/lib/server-runtime/server.ts+12 −6 modified@@ -23,7 +23,7 @@ import { matchServerRoutes } from "./routeMatching"; import type { ServerRoute } from "./routes"; import { createStaticHandlerDataRoutes, createRoutes } from "./routes"; import { createServerHandoffString } from "./serverHandoff"; -import { getDevServerHooks } from "./dev"; +import { getBuildTimeHeader, getDevServerHooks } from "./dev"; import { encodeViaTurboStream, getSingleFetchRedirect, @@ -164,12 +164,17 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( normalizedPath = normalizedPath.slice(0, -1); } + let isSpaMode = + getBuildTimeHeader(request, "X-React-Router-SPA-Mode") === "yes"; + // When runtime SSR is disabled, make our dev server behave like the deployed // pre-rendered site would if (!_build.ssr) { + // When SSR is disabled this, file can only ever run during dev because we + // delete the server build at the end of the build if (_build.prerender.length === 0) { - // Add the header if we're in SPA mode - request.headers.set("X-React-Router-SPA-Mode", "yes"); + // ssr:false and no prerender config indicates "SPA Mode" + isSpaMode = true; } else if ( !_build.prerender.includes(normalizedPath) && !_build.prerender.includes(normalizedPath + "/") @@ -194,7 +199,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( }); } else { // Serve a SPA fallback for non-pre-rendered document requests - request.headers.set("X-React-Router-SPA-Mode", "yes"); + isSpaMode = true; } } } @@ -275,7 +280,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( } } } else if ( - !request.headers.has("X-React-Router-SPA-Mode") && + !isSpaMode && matches && matches[matches.length - 1].route.module.default == null && matches[matches.length - 1].route.module.ErrorBoundary == null @@ -309,6 +314,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( request, loadContext, handleError, + isSpaMode, criticalCss ); } @@ -426,9 +432,9 @@ async function handleDocumentRequest( request: Request, loadContext: AppLoadContext | unstable_RouterContextProvider, handleError: (err: unknown) => void, + isSpaMode: boolean, criticalCss?: CriticalCss ) { - let isSpaMode = request.headers.has("X-React-Router-SPA-Mode"); try { let response = await staticHandler.query(request, { requestContext: loadContext,
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-f46r-rw29-r322ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-43864ghsaADVISORY
- github.com/remix-run/react-router/blob/e6c53a0130559b4a9bd47f9cf76ea5b08a69868a/packages/react-router/lib/server-runtime/server.tsnvdWEB
- github.com/remix-run/react-router/commit/c84302972a152d851cf5dd859ff332b354b70111nvdWEB
- github.com/remix-run/react-router/security/advisories/GHSA-f46r-rw29-r322nvdWEB
News mentions
0No linked articles in our index yet.