Fix OIDC redirect forwarded port handling (#1007)

This commit is contained in:
ZacharyZcR
2026-07-10 08:03:59 +08:00
committed by GitHub
parent 4f05bd4fbd
commit 85130dc056
4 changed files with 104 additions and 31 deletions
+50
View File
@@ -3,6 +3,7 @@ import {
getRequestBasePath,
getRequestBaseUrl,
getRequestBaseUrlWithForceHTTPS,
getRequestOrigin,
normalizeBasePath,
} from "./request-origin.js";
@@ -106,3 +107,52 @@ describe("getRequestBasePath", () => {
).toBe("https://example.com/termix");
});
});
describe("getRequestOrigin", () => {
it("ignores non-numeric forwarded ports", () => {
expect(
getRequestOrigin(
request({
"x-forwarded-proto": "https",
"x-forwarded-host": "termix.test.de",
"x-forwarded-port": "{server_port}",
}),
),
).toBe("https://termix.test.de");
});
it("drops invalid ports embedded in forwarded hosts", () => {
expect(
getRequestOrigin(
request({
"x-forwarded-proto": "https",
"x-forwarded-host": "termix.test.de:{server_port}",
}),
),
).toBe("https://termix.test.de");
});
it("keeps valid non-default forwarded ports", () => {
expect(
getRequestOrigin(
request({
"x-forwarded-proto": "https",
"x-forwarded-host": "termix.test.de",
"x-forwarded-port": "8443",
}),
),
).toBe("https://termix.test.de:8443");
});
it("omits default forwarded ports", () => {
expect(
getRequestOrigin(
request({
"x-forwarded-proto": "https",
"x-forwarded-host": "termix.test.de",
"x-forwarded-port": "443",
}),
),
).toBe("https://termix.test.de");
});
});
+46 -23
View File
@@ -7,6 +7,44 @@ function firstHeaderValue(value: string | string[] | undefined): string {
return raw.split(",")[0].trim();
}
function normalizePort(value: string | string[] | undefined): string {
const raw = firstHeaderValue(value);
if (!/^\d+$/.test(raw)) return "";
const port = Number(raw);
if (!Number.isInteger(port) || port < 1 || port > 65535) return "";
return String(port);
}
function splitHostHeader(value: string | string[] | undefined): {
host: string;
port: string;
} {
const raw = firstHeaderValue(value) || "localhost";
if (raw.startsWith("[")) {
const match = raw.match(/^(\[[^\]]+\])(?::(.+))?$/);
return {
host: match?.[1] || raw,
port: normalizePort(match?.[2]),
};
}
const parts = raw.split(":");
if (parts.length === 2) {
return {
host: parts[0] || "localhost",
port: normalizePort(parts[1]),
};
}
return {
host: raw,
port: "",
};
}
export function normalizeBasePath(value: unknown): string {
if (typeof value !== "string") return "";
let basePath = value.split(",")[0].trim();
@@ -45,38 +83,23 @@ export function getRequestOrigin(req: Request | IncomingMessage): string {
: "http";
}
const portHeader = req.headers["x-forwarded-port"];
let port: string | undefined =
typeof portHeader === "string"
? portHeader.split(",")[0].trim()
: undefined;
let port = normalizePort(req.headers["x-forwarded-port"]);
const { host, port: hostPort } = splitHostHeader(
req.headers["x-forwarded-host"] || req.headers.host,
);
port ||= hostPort;
const hostHeaderRaw =
req.headers["x-forwarded-host"] || req.headers.host || "localhost";
const hostHeader =
typeof hostHeaderRaw === "string"
? hostHeaderRaw.split(",")[0].trim()
: String(hostHeaderRaw);
if (!port && hostHeader.includes(":")) {
const parts = hostHeader.split(":");
if (parts.length === 2 && !parts[0].includes("[")) {
port = parts[1];
}
}
const hostWithoutPort = hostHeader.split(":")[0];
if (port) {
const isDefaultPort =
(protocol === "http" && port === "80") ||
(protocol === "https" && port === "443");
return isDefaultPort
? `${protocol}://${hostWithoutPort}`
: `${protocol}://${hostWithoutPort}:${port}`;
? `${protocol}://${host}`
: `${protocol}://${host}:${port}`;
}
return `${protocol}://${hostWithoutPort}`;
return `${protocol}://${host}`;
}
export function getRequestOriginWithForceHTTPS(