From ec4622a1b8cb65c87672559020631d715e86d651 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sun, 21 Jun 2026 05:39:35 +0300 Subject: [PATCH] test(security): export + unit-test resolveTrustProxy (#105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocate resolveTrustProxy from main.ts (untestable — bootstraps on import) to integrations/environment/trust-proxy.util.ts and import it back. Unit-test every branch (empty/undefined -> safe loopback/private default; true/false; hop count; trim; CIDR/negative passthrough) so a regression can't silently re-open the XFF spoofing hole (#61). Co-Authored-By: Claude Opus 4.8 --- .../environment/trust-proxy.util.spec.ts | 50 +++++++++++++++++++ .../environment/trust-proxy.util.ts | 14 ++++++ apps/server/src/main.ts | 14 +----- 3 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 apps/server/src/integrations/environment/trust-proxy.util.spec.ts create mode 100644 apps/server/src/integrations/environment/trust-proxy.util.ts 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(