From 28cb678934a93645d242aad4b80fb6579c3d3a01 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sat, 26 Sep 2026 22:21:14 +0000 Subject: [PATCH] fix(config): normalize trustProxy lists --- src/define_config.ts | 21 +++++++++++++++------ tests/define_config.spec.ts | 34 ++++++++++++++++++++++++++++++++++ tests/request.spec.ts | 14 ++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/define_config.ts b/src/define_config.ts index 682964e..dfd66bd 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -17,7 +17,7 @@ import type { ServerConfig } from './types/server.ts' type UserDefinedServerConfig = DeepPartial< Omit & { - trustProxy: ((address: string, distance: number) => boolean) | boolean | string + trustProxy: ((address: string, distance: number) => boolean) | boolean | string | string[] } > @@ -28,6 +28,9 @@ type UserDefinedServerConfig = DeepPartial< * normalizes certain properties (like cookie maxAge and trustProxy settings), * and returns a complete ServerConfig object. * + * The `trustProxy` option accepts a range string, a comma-separated list of + * ranges, or an array of ranges. It also accepts a boolean or a custom function. + * * @param config - User-defined server configuration options * * @example @@ -103,15 +106,21 @@ export function defineConfig(config: UserDefinedServerConfig): ServerConfig { } /** - * Normalizing trust proxy setting to allow boolean and - * string values + * Normalizing trust proxy setting to allow boolean, string, + * and array values */ if (typeof trustProxy === 'boolean') { const tpValue = trustProxy normalizedConfig.trustProxy = (_, __) => tpValue - } else if (typeof trustProxy === 'string') { - const tpValue = trustProxy - normalizedConfig.trustProxy = proxyAddr.compile(tpValue) + } else if (typeof trustProxy === 'string' || Array.isArray(trustProxy)) { + normalizedConfig.trustProxy = proxyAddr.compile( + typeof trustProxy === 'string' + ? trustProxy + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + : trustProxy + ) } else if (trustProxy) { normalizedConfig.trustProxy = trustProxy } diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts index ab5c898..87a5871 100644 --- a/tests/define_config.spec.ts +++ b/tests/define_config.spec.ts @@ -71,6 +71,40 @@ test.group('Define config', () => { const config = defineConfig({ trustProxy: 'loopback' }) assert.typeOf(config.trustProxy, 'function') + assert.isTrue(config.trustProxy('127.0.0.1', 0)) + assert.isFalse(config.trustProxy('10.20.30.40', 0)) + }) + + test('compile comma-separated trustProxy config', ({ assert }) => { + const config = defineConfig({ + trustProxy: 'loopback, 10.20.0.0/16, 2001:db8::/32', + }) + + assert.isTrue(config.trustProxy('127.0.0.1', 0)) + assert.isTrue(config.trustProxy('10.20.30.40', 0)) + assert.isTrue(config.trustProxy('2001:db8::1', 0)) + assert.isFalse(config.trustProxy('192.168.1.1', 0)) + }) + + test('compile trustProxy config when an array', ({ assert }) => { + const config = defineConfig({ trustProxy: ['loopback', '2001:db8::/32'] }) + + assert.isTrue(config.trustProxy('127.0.0.1', 0)) + assert.isTrue(config.trustProxy('2001:db8::1', 0)) + assert.isFalse(config.trustProxy('10.20.30.40', 0)) + }) + + test('trust no proxies when a comma-separated string has no ranges', ({ assert }) => { + const config = defineConfig({ trustProxy: ' , ' }) + + assert.isFalse(config.trustProxy('127.0.0.1', 0)) + }) + + test('throw when a trustProxy range is invalid', ({ assert }) => { + assert.throws( + () => defineConfig({ trustProxy: 'loopback, invalid' }), + 'invalid IP address: invalid' + ) }) test('compile trustProxy config when a function', ({ assert }) => { diff --git a/tests/request.spec.ts b/tests/request.spec.ts index 153cd9a..887a26d 100644 --- a/tests/request.spec.ts +++ b/tests/request.spec.ts @@ -18,6 +18,7 @@ import { EncryptionFactory } from '@boringnode/encryption/factories' import { RouterFactory } from '../factories/router.ts' import { HttpRequestFactory } from '../factories/request.ts' import { httpServer } from '../factories/http_server.ts' +import { defineConfig } from '../index.ts' import { CookieSerializer } from '../src/cookies/serializer.ts' import { HttpContextFactory } from '../factories/http_context.ts' @@ -612,6 +613,19 @@ test.group('Request', () => { assert.equal(body.ip, '10.10.10.10') }) + test('trust proxy from an array in the defined config', async ({ assert }) => { + const config = defineConfig({ trustProxy: ['loopback'] }) + const { url } = await httpServer.create((req, res) => { + req.headers['x-forwarded-for'] = '10.10.10.10' + const request = new HttpRequestFactory().merge({ req, res, encryption, config }).create() + res.writeHead(200, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ ip: request.ip() })) + }) + + const { body } = await supertest(url).get('/') + assert.equal(body.ip, '10.10.10.10') + }) + test('use the first forwarded host from a trusted proxy', async ({ assert }) => { const { url } = await httpServer.create((req, res) => { const request = new HttpRequestFactory()