test(security): export + unit-test resolveTrustProxy (#105)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
14
apps/server/src/integrations/environment/trust-proxy.util.ts
Normal file
14
apps/server/src/integrations/environment/trust-proxy.util.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<NestFastifyApplication>(
|
||||
|
||||
Reference in New Issue
Block a user