Skip to content

Commit b75423d

Browse files
authored
fix(@angular/ssr): support all X-Forwarded-* headers when trustProxyHeaders is true
Previously, setting `trustProxyHeaders: true` only allowed a predefined set of common proxy headers (such as `x-forwarded-for` and `x-forwarded-host`). This resulted in warning logs when requests contained other valid proxy headers like `x-forwarded-client-cert` or `x-forwarded-email`.
1 parent c6d1d5d commit b75423d

2 files changed

Lines changed: 67 additions & 20 deletions

File tree

packages/angular/ssr/src/utils/validation.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,9 @@
77
*/
88

99
/**
10-
* Common X-Forwarded-* headers.
10+
* Internal sentinel string representing a wildcard rule to trust all proxy headers.
1111
*/
12-
const X_FORWARDED_HEADERS: ReadonlySet<string> = new Set([
13-
'x-forwarded-for',
14-
'x-forwarded-host',
15-
'x-forwarded-port',
16-
'x-forwarded-proto',
17-
'x-forwarded-prefix',
18-
]);
12+
const TRUST_ALL_PROXY_HEADERS = '*';
1913

2014
/**
2115
* The set of headers that should be validated for host header injection attacks.
@@ -235,7 +229,10 @@ export function isProxyHeaderAllowed(
235229
headerName: string,
236230
trustProxyHeaders: ReadonlySet<string>,
237231
): boolean {
238-
return trustProxyHeaders.has(headerName.toLowerCase());
232+
return (
233+
trustProxyHeaders.has(TRUST_ALL_PROXY_HEADERS) ||
234+
trustProxyHeaders.has(headerName.toLowerCase())
235+
);
239236
}
240237

241238
/**
@@ -251,8 +248,15 @@ export function normalizeTrustProxyHeaders(
251248
}
252249

253250
if (trustProxyHeaders === true) {
254-
return X_FORWARDED_HEADERS;
251+
return new Set([TRUST_ALL_PROXY_HEADERS]);
255252
}
256253

257-
return new Set(trustProxyHeaders.map((h) => h.toLowerCase()));
254+
const normalizedTrustedProxyHeaders = new Set(trustProxyHeaders.map((h) => h.toLowerCase()));
255+
if (normalizedTrustedProxyHeaders.has(TRUST_ALL_PROXY_HEADERS)) {
256+
throw new Error(
257+
`"${TRUST_ALL_PROXY_HEADERS}" is not allowed as a value for the "trustProxyHeaders" option.`,
258+
);
259+
}
260+
261+
return normalizedTrustedProxyHeaders;
258262
}

packages/angular/ssr/test/utils/validation_spec.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {
1010
getFirstHeaderValue,
11+
normalizeTrustProxyHeaders,
1112
sanitizeRequestHeaders,
1213
validateRequest,
1314
validateUrl,
@@ -37,6 +38,35 @@ describe('Validation Utils', () => {
3738
});
3839
});
3940

41+
describe('normalizeTrustProxyHeaders', () => {
42+
it('should return an empty set when input is undefined', () => {
43+
expect(normalizeTrustProxyHeaders(undefined)).toEqual(new Set());
44+
});
45+
46+
it('should return an empty set when input is false', () => {
47+
expect(normalizeTrustProxyHeaders(false)).toEqual(new Set());
48+
});
49+
50+
it('should return a set containing "*" when input is true', () => {
51+
expect(normalizeTrustProxyHeaders(true)).toEqual(new Set(['*']));
52+
});
53+
54+
it('should return a set of lowercased header names when input is an array of strings', () => {
55+
expect(normalizeTrustProxyHeaders(['X-Forwarded-Host', 'X-Forwarded-Proto'])).toEqual(
56+
new Set(['x-forwarded-host', 'x-forwarded-proto']),
57+
);
58+
});
59+
60+
it('should throw an error if input array contains "*"', () => {
61+
expect(() => normalizeTrustProxyHeaders(['*'])).toThrowError(
62+
'"*" is not allowed as a value for the "trustProxyHeaders" option.',
63+
);
64+
expect(() => normalizeTrustProxyHeaders(['X-Forwarded-Host', '*'])).toThrowError(
65+
'"*" is not allowed as a value for the "trustProxyHeaders" option.',
66+
);
67+
});
68+
});
69+
4070
describe('validateUrl', () => {
4171
const allowedHosts = new Set(['example.com', '*.google.com']);
4272

@@ -224,23 +254,37 @@ describe('Validation Utils', () => {
224254
'x-forwarded-proto': 'https',
225255
},
226256
});
227-
const secured = sanitizeRequestHeaders(req, new Set());
257+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined));
228258

229259
expect(secured.headers.get('host')).toBe('example.com');
230260
expect(secured.headers.has('x-forwarded-host')).toBeFalse();
231261
expect(secured.headers.has('x-forwarded-proto')).toBeFalse();
232262
});
233263

234-
it('should retain allowed proxy headers when explicitly provided', () => {
235-
const trustProxyHeaders = new Set(['x-forwarded-host']);
264+
it('should scrub unallowed proxy headers when trustProxyHeaders is false', () => {
265+
const req = new Request('http://example.com', {
266+
headers: {
267+
'host': 'example.com',
268+
'x-forwarded-host': 'evil.com',
269+
'x-forwarded-proto': 'https',
270+
},
271+
});
272+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(false));
273+
274+
expect(secured.headers.get('host')).toBe('example.com');
275+
expect(secured.headers.has('x-forwarded-host')).toBeFalse();
276+
expect(secured.headers.has('x-forwarded-proto')).toBeFalse();
277+
});
278+
279+
it('should only retain allowed proxy headers when explicitly provided', () => {
236280
const req = new Request('http://example.com', {
237281
headers: {
238282
'host': 'example.com',
239283
'x-forwarded-host': 'proxy.com',
240284
'x-forwarded-proto': 'https',
241285
},
242286
});
243-
const secured = sanitizeRequestHeaders(req, trustProxyHeaders);
287+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(['x-forwarded-host']));
244288

245289
expect(secured.headers.get('host')).toBe('example.com');
246290
expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com');
@@ -253,23 +297,22 @@ describe('Validation Utils', () => {
253297
'host': 'example.com',
254298
'x-forwarded-host': 'proxy.com',
255299
'x-forwarded-proto': 'https',
300+
'x-forwarded-email': 'user@example.com',
256301
},
257302
});
258-
const secured = sanitizeRequestHeaders(
259-
req,
260-
new Set(['x-forwarded-host', 'x-forwarded-proto']),
261-
);
303+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(true));
262304

263305
expect(secured.headers.get('host')).toBe('example.com');
264306
expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com');
265307
expect(secured.headers.get('x-forwarded-proto')).toBe('https');
308+
expect(secured.headers.get('x-forwarded-email')).toBe('user@example.com');
266309
});
267310

268311
it('should not clone the request if no proxy headers need to be removed', () => {
269312
const req = new Request('http://example.com', {
270313
headers: { 'accept': 'application/json' },
271314
});
272-
const secured = sanitizeRequestHeaders(req, new Set());
315+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(false));
273316

274317
expect(secured).toBe(req);
275318
expect(secured.headers.get('accept')).toBe('application/json');

0 commit comments

Comments
 (0)