fix(server): honor proxy trust for forwarded host (#12832)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-04 10:02:28 -05:00 committed by GitHub
parent 4ef6155aae
commit 7dfc769f3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 2 deletions

View File

@ -92,6 +92,7 @@ describe("boardMutationGuard", () => {
it("allows HTTPS branch-runtime mutations when forwarded MagicDNS host and non-standard port match", async () => {
const app = createApp("board");
app.set("trust proxy", "loopback");
const res = await request(app)
.post("/mutate")
.set("Host", "127.0.0.1")
@ -102,6 +103,17 @@ describe("boardMutationGuard", () => {
expect([200, 204]).toContain(res.status);
});
it("ignores x-forwarded-host from an untrusted direct client", async () => {
const app = createApp("board");
const res = await request(app)
.post("/mutate")
.set("Host", "board.example.test")
.set("X-Forwarded-Host", "attacker.example.test")
.set("Origin", "https://attacker.example.test")
.send({ ok: true });
expect(res.status).toBe(403);
});
it("blocks board mutations when x-forwarded-host does not match origin", async () => {
const middleware = boardMutationGuard();
const req = {

View File

@ -16,10 +16,33 @@ function parseOrigin(value: string | undefined) {
}
}
/**
* Resolve the host used for same-origin checks without letting a direct client
* promote its own X-Forwarded-Host value into the trusted-origin set. Express
* compiles the operator's TRUST_PROXY setting into `trust proxy fn`; only a
* trusted immediate peer may supply the forwarded host.
*/
function requestHost(req: Request): string | undefined {
const host = req.header("host")?.trim();
const remoteAddress = req.socket?.remoteAddress;
const trustProxy = req.app?.get("trust proxy fn") as
| ((address: string, hop: number) => boolean)
| undefined;
if (
remoteAddress
&& typeof trustProxy === "function"
&& trustProxy(remoteAddress, 0)
) {
return req.header("x-forwarded-host")?.split(",")[0]?.trim() || host;
}
return host;
}
function trustedOriginsForRequest(req: Request) {
const origins = new Set(DEFAULT_DEV_ORIGINS.map((value) => value.toLowerCase()));
const forwardedHost = req.header("x-forwarded-host")?.split(",")[0]?.trim();
const host = forwardedHost || req.header("host")?.trim();
const host = requestHost(req);
if (host) {
origins.add(`http://${host}`.toLowerCase());
origins.add(`https://${host}`.toLowerCase());