High severity8.2NVD Advisory· Published Apr 25, 2025· Updated Apr 15, 2026
CVE-2025-43865
CVE-2025-43865
Description
React Router is a router for React. In versions on the 7.0 branch prior to version 7.5.2, it's possible to modify pre-rendered data by adding a header to the request. This allows to completely spoof its contents and modify all the values of the data object passed to the HTML. 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.0.0-pre.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-cpj6-fhp6-mr6jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-43865ghsaADVISORY
- github.com/remix-run/react-router/blob/e6c53a0130559b4a9bd47f9cf76ea5b08a69868a/packages/react-router/lib/server-runtime/routes.tsnvdWEB
- github.com/remix-run/react-router/commit/c84302972a152d851cf5dd859ff332b354b70111nvdWEB
- github.com/remix-run/react-router/security/advisories/GHSA-cpj6-fhp6-mr6jnvdWEB
News mentions
0No linked articles in our index yet.