fix: harden HTTP trust boundaries (#1316)

This commit is contained in:
ZacharyZcR
2026-08-24 07:52:31 +08:00
committed by GitHub
parent c4c9b51294
commit 30d72554fc
23 changed files with 167 additions and 62 deletions
+5 -5
View File
@@ -61,7 +61,7 @@ describe("logAudit", () => {
});
describe("getRequestMeta", () => {
it("extracts ip from x-forwarded-for header", () => {
it("uses the proxy-validated Express IP", () => {
const req = {
headers: {
"x-forwarded-for": "10.0.0.1, 10.0.0.2",
@@ -71,7 +71,7 @@ describe("getRequestMeta", () => {
socket: {},
};
const meta = getRequestMeta(req as never);
expect(meta.ipAddress).toBe("10.0.0.1");
expect(meta.ipAddress).toBe("127.0.0.1");
expect(meta.userAgent).toBe("TestAgent/1.0");
});
@@ -85,16 +85,16 @@ describe("getRequestMeta", () => {
expect(meta.ipAddress).toBe("192.168.1.1");
});
it("splits and trims a forwarded header sent as an array", () => {
it("ignores an unvalidated forwarded header", () => {
const req = {
headers: {
"x-forwarded-for": ["10.0.0.1, 10.0.0.2"],
"user-agent": "TestAgent/1.0",
},
socket: {},
socket: { remoteAddress: "203.0.113.9" },
};
const meta = getRequestMeta(req as never);
expect(meta.ipAddress).toBe("10.0.0.1");
expect(meta.ipAddress).toBe("203.0.113.9");
});
it("falls back to the socket peer when there is no forwarded header or req.ip", () => {
@@ -33,7 +33,7 @@ describe("getAuditUsername", () => {
});
describe("getRequestMeta", () => {
it("prefers the first x-forwarded-for hop", () => {
it("prefers the proxy-validated Express IP", () => {
const meta = getRequestMeta({
headers: {
"x-forwarded-for": "203.0.113.9, 10.0.0.1",
@@ -43,7 +43,7 @@ describe("getRequestMeta", () => {
} as never);
expect(meta).toEqual({
ipAddress: "203.0.113.9",
ipAddress: "10.0.0.1",
userAgent: "Mozilla/5.0",
});
});
@@ -0,0 +1,48 @@
import { afterEach, describe, expect, it } from "vitest";
import type { Request } from "express";
import { isCorsOriginAllowed } from "../../utils/cors-config.js";
function request(headers: Record<string, string> = {}): Request {
return {
headers,
protocol: "http",
} as unknown as Request;
}
afterEach(() => {
delete process.env.CORS_ALLOWED_ORIGINS;
});
describe("isCorsOriginAllowed", () => {
it("allows requests without an Origin header", () => {
expect(isCorsOriginAllowed(request(), undefined)).toBe(true);
});
it("allows the externally forwarded same origin", () => {
const req = request({
"x-forwarded-proto": "https",
"x-forwarded-host": "termix.example",
});
expect(isCorsOriginAllowed(req, "https://termix.example")).toBe(true);
});
it("rejects an unrelated origin even when the TCP peer is loopback", () => {
const req = {
...request({ host: "termix.example" }),
socket: { remoteAddress: "127.0.0.1" },
} as unknown as Request;
expect(isCorsOriginAllowed(req, "https://attacker.example")).toBe(false);
});
it("allows an explicitly configured origin", () => {
process.env.CORS_ALLOWED_ORIGINS = "https://portal.example";
expect(isCorsOriginAllowed(request(), "https://portal.example")).toBe(true);
});
it("does not allow a wildcard with credentialed requests", () => {
process.env.CORS_ALLOWED_ORIGINS = "*";
expect(isCorsOriginAllowed(request(), "https://attacker.example")).toBe(
false,
);
});
});
@@ -122,26 +122,27 @@ describe("getRequestBasePath", () => {
});
describe("getClientIp", () => {
it("prefers the leftmost X-Forwarded-For entry over the socket peer", () => {
it("uses Express's proxy-validated req.ip", () => {
expect(
getClientIp(
requestWithSocket(
{ "x-forwarded-for": "203.0.113.7, 10.0.0.1, 10.0.0.2" },
{ remoteAddress: "::ffff:127.0.0.1" },
"198.51.100.5",
),
),
).toBe("203.0.113.7");
).toBe("198.51.100.5");
});
it("handles X-Forwarded-For sent as a header array", () => {
it("does not trust a raw forwarded header without Express validation", () => {
expect(
getClientIp(
requestWithSocket(
{ "x-forwarded-for": ["203.0.113.7", "10.0.0.1"] },
{ remoteAddress: "::ffff:127.0.0.1" },
{ "x-forwarded-for": "127.0.0.1" },
{ remoteAddress: "198.51.100.9" },
),
),
).toBe("203.0.113.7");
).toBe("198.51.100.9");
});
it("falls back to req.ip when there is no forwarded header", () => {