diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index d2e38f5b..668df97c 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -433,8 +433,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; + proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; @@ -676,8 +676,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; + proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; diff --git a/docker/nginx.conf b/docker/nginx.conf index a8a44e2a..68cff5a6 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -434,8 +434,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; + proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; @@ -677,8 +677,8 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; + proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; diff --git a/src/backend/utils/request-origin.test.ts b/src/backend/utils/request-origin.test.ts index d569f6e9..711de73a 100644 --- a/src/backend/utils/request-origin.test.ts +++ b/src/backend/utils/request-origin.test.ts @@ -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"); + }); +}); diff --git a/src/backend/utils/request-origin.ts b/src/backend/utils/request-origin.ts index 1f91d1cf..7ecf54eb 100644 --- a/src/backend/utils/request-origin.ts +++ b/src/backend/utils/request-origin.ts @@ -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(