diff --git a/apps/server/src/integrations/environment/trust-proxy.util.spec.ts b/apps/server/src/integrations/environment/trust-proxy.util.spec.ts new file mode 100644 index 00000000..294c6fba --- /dev/null +++ b/apps/server/src/integrations/environment/trust-proxy.util.spec.ts @@ -0,0 +1,50 @@ +import { resolveTrustProxy } from './trust-proxy.util'; + +/** + * Unit tests for resolveTrustProxy: the helper that turns the TRUST_PROXY env + * string into a Fastify trustProxy value. The contract is: empty/undefined + * falls back to the safe loopback/linklocal/uniquelocal default (so a public-IP + * client cannot spoof X-Forwarded-For); 'true'/'false' become booleans; a + * non-negative integer becomes a hop count (number); anything else (CIDR/IP + * lists, negative numbers, named keywords) is passed through verbatim as a + * trimmed string. + */ +describe('resolveTrustProxy', () => { + const SAFE_DEFAULT = 'loopback, linklocal, uniquelocal'; + + it('returns the safe default for an empty string', () => { + expect(resolveTrustProxy('')).toBe(SAFE_DEFAULT); + }); + + it('returns the safe default for undefined', () => { + expect(resolveTrustProxy(undefined)).toBe(SAFE_DEFAULT); + }); + + it("returns the boolean true for 'true'", () => { + expect(resolveTrustProxy('true')).toBe(true); + }); + + it("returns the boolean false for 'false'", () => { + expect(resolveTrustProxy('false')).toBe(false); + }); + + it("returns the number 2 for '2'", () => { + expect(resolveTrustProxy('2')).toBe(2); + }); + + it("trims surrounding whitespace and returns the number 3 for ' 3 '", () => { + expect(resolveTrustProxy(' 3 ')).toBe(3); + }); + + it('passes a CIDR string through unchanged', () => { + expect(resolveTrustProxy('10.0.0.0/8')).toBe('10.0.0.0/8'); + }); + + it("passes a negative number through as a string ('-1' is not a valid hop count)", () => { + expect(resolveTrustProxy('-1')).toBe('-1'); + }); + + it('passes a non-numeric keyword through unchanged', () => { + expect(resolveTrustProxy('loopback')).toBe('loopback'); + }); +}); diff --git a/apps/server/src/integrations/environment/trust-proxy.util.ts b/apps/server/src/integrations/environment/trust-proxy.util.ts new file mode 100644 index 00000000..176e0654 --- /dev/null +++ b/apps/server/src/integrations/environment/trust-proxy.util.ts @@ -0,0 +1,14 @@ +// Trust X-Forwarded-For ONLY from real proxies on private/loopback nets by +// default, so a public-IP client cannot spoof its IP via X-Forwarded-For. +// TRUST_PROXY env overrides: 'true'/'false', a hop count (integer), or a +// CIDR/IP list string passed through to Fastify/proxy-addr. +export function resolveTrustProxy( + rawInput?: string, +): boolean | number | string { + const raw = rawInput?.trim(); + if (raw == null || raw === '') return 'loopback, linklocal, uniquelocal'; + if (raw === 'true') return true; + if (raw === 'false') return false; + const n = Number(raw); + return Number.isInteger(n) && n >= 0 ? n : raw; +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 3588f045..05968d09 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -14,19 +14,7 @@ import fastifyIp from 'fastify-ip'; import { InternalLogFilter } from './common/logger/internal-log-filter'; import { EnvironmentService } from './integrations/environment/environment.service'; import { resolveFrameHeader } from './common/helpers'; - -// Trust X-Forwarded-For ONLY from real proxies on private/loopback nets by -// default, so a public-IP client cannot spoof its IP via X-Forwarded-For. -// TRUST_PROXY env overrides: 'true'/'false', a hop count (integer), or a -// CIDR/IP list string passed through to Fastify/proxy-addr. -function resolveTrustProxy(rawInput?: string): boolean | number | string { - const raw = rawInput?.trim(); - if (raw == null || raw === '') return 'loopback, linklocal, uniquelocal'; - if (raw === 'true') return true; - if (raw === 'false') return false; - const n = Number(raw); - return Number.isInteger(n) && n >= 0 ? n : raw; -} +import { resolveTrustProxy } from './integrations/environment/trust-proxy.util'; async function bootstrap() { const app = await NestFactory.create(