diff --git a/packages/react-router-dev/.changes/patch.trust-dev-proxy-headers.md b/packages/react-router-dev/.changes/patch.trust-dev-proxy-headers.md new file mode 100644 index 0000000000..0611c08e6a --- /dev/null +++ b/packages/react-router-dev/.changes/patch.trust-dev-proxy-headers.md @@ -0,0 +1 @@ +Respect trusted reverse proxy host and protocol headers in the development server diff --git a/packages/react-router-dev/vite/node-adapter-test.ts b/packages/react-router-dev/vite/node-adapter-test.ts new file mode 100644 index 0000000000..9c215e64d6 --- /dev/null +++ b/packages/react-router-dev/vite/node-adapter-test.ts @@ -0,0 +1,47 @@ +import { + type AddressInfo, + createServer, + request as sendRequest, +} from "node:http"; +import type * as Vite from "vite"; +import { fromNodeRequest } from "./node-adapter"; + +describe("fromNodeRequest", () => { + it("uses the forwarded protocol without trusting the forwarded host", async () => { + let requestUrl = await new Promise((resolve, reject) => { + let server = createServer(async (nodeReq, nodeRes) => { + let viteRequest = Object.assign(nodeReq, { + originalUrl: nodeReq.url, + }) as Vite.Connect.IncomingMessage; + + try { + let request = await fromNodeRequest(viteRequest, nodeRes); + resolve(request.url); + nodeRes.end(); + } catch (error) { + reject(error); + } finally { + server.close(); + } + }); + + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + let { port } = server.address() as AddressInfo; + let request = sendRequest({ + host: "127.0.0.1", + port, + headers: { + host: "public.example.com", + "x-forwarded-host": "evil.example.com", + "x-forwarded-proto": "https", + }, + }); + request.on("error", reject); + request.end(); + }); + }); + + expect(requestUrl).toBe("https://public.example.com/"); + }); +}); diff --git a/packages/react-router-dev/vite/node-adapter.ts b/packages/react-router-dev/vite/node-adapter.ts index 625e12d77c..9a2e2b679f 100644 --- a/packages/react-router-dev/vite/node-adapter.ts +++ b/packages/react-router-dev/vite/node-adapter.ts @@ -19,5 +19,10 @@ export async function fromNodeRequest( ); nodeReq.url = nodeReq.originalUrl; - return createRequest(nodeReq, nodeRes); + return createRequest(nodeReq, nodeRes, { + // Vite validates the Host header against server.allowedHosts. Keep that + // validated host instead of trusting a client-controlled forwarded host. + host: nodeReq.headers.host ?? "localhost", + trustProxy: true, + }); }