Skip to content

Commit 0aeebba

Browse files
committed
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`. Closes #33169
1 parent 3ab0152 commit 0aeebba

2 files changed

Lines changed: 30 additions & 19 deletions

File tree

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

Lines changed: 7 additions & 10 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,7 +248,7 @@ export function normalizeTrustProxyHeaders(
251248
}
252249

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

257254
return new Set(trustProxyHeaders.map((h) => h.toLowerCase()));

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

Lines changed: 23 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,
@@ -224,23 +225,37 @@ describe('Validation Utils', () => {
224225
'x-forwarded-proto': 'https',
225226
},
226227
});
227-
const secured = sanitizeRequestHeaders(req, new Set());
228+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined));
228229

229230
expect(secured.headers.get('host')).toBe('example.com');
230231
expect(secured.headers.has('x-forwarded-host')).toBeFalse();
231232
expect(secured.headers.has('x-forwarded-proto')).toBeFalse();
232233
});
233234

234-
it('should retain allowed proxy headers when explicitly provided', () => {
235-
const trustProxyHeaders = new Set(['x-forwarded-host']);
235+
it('should scrub unallowed proxy headers when trustProxyHeaders is false', () => {
236+
const req = new Request('http://example.com', {
237+
headers: {
238+
'host': 'example.com',
239+
'x-forwarded-host': 'evil.com',
240+
'x-forwarded-proto': 'https',
241+
},
242+
});
243+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(false));
244+
245+
expect(secured.headers.get('host')).toBe('example.com');
246+
expect(secured.headers.has('x-forwarded-host')).toBeFalse();
247+
expect(secured.headers.has('x-forwarded-proto')).toBeFalse();
248+
});
249+
250+
it('should only retain allowed proxy headers when explicitly provided', () => {
236251
const req = new Request('http://example.com', {
237252
headers: {
238253
'host': 'example.com',
239254
'x-forwarded-host': 'proxy.com',
240255
'x-forwarded-proto': 'https',
241256
},
242257
});
243-
const secured = sanitizeRequestHeaders(req, trustProxyHeaders);
258+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(['x-forwarded-host']));
244259

245260
expect(secured.headers.get('host')).toBe('example.com');
246261
expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com');
@@ -253,23 +268,22 @@ describe('Validation Utils', () => {
253268
'host': 'example.com',
254269
'x-forwarded-host': 'proxy.com',
255270
'x-forwarded-proto': 'https',
271+
'x-forwarded-email': 'user@example.com',
256272
},
257273
});
258-
const secured = sanitizeRequestHeaders(
259-
req,
260-
new Set(['x-forwarded-host', 'x-forwarded-proto']),
261-
);
274+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(true));
262275

263276
expect(secured.headers.get('host')).toBe('example.com');
264277
expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com');
265278
expect(secured.headers.get('x-forwarded-proto')).toBe('https');
279+
expect(secured.headers.get('x-forwarded-email')).toBe('user@example.com');
266280
});
267281

268282
it('should not clone the request if no proxy headers need to be removed', () => {
269283
const req = new Request('http://example.com', {
270284
headers: { 'accept': 'application/json' },
271285
});
272-
const secured = sanitizeRequestHeaders(req, new Set());
286+
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(false));
273287

274288
expect(secured).toBe(req);
275289
expect(secured.headers.get('accept')).toBe('application/json');

0 commit comments

Comments
 (0)