Skip to content

Commit 1c411bd

Browse files
committed
fix(@angular/ssr): normalize patched host headers
Return the same first host-header token that is validated so application code cannot receive unvalidated forwarded-host suffixes.
1 parent 84f645f commit 1c411bd

2 files changed

Lines changed: 49 additions & 3 deletions

File tree

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,16 @@ export function cloneRequestAndPatchHeaders(
9898
});
9999

100100
const headers = clonedReq.headers;
101-
102101
const originalGet = headers.get;
102+
103+
for (const name of HOST_HEADERS_TO_VALIDATE) {
104+
const value = originalGet.call(headers, name);
105+
const normalizedValue = getFirstHeaderValue(value);
106+
if (normalizedValue !== undefined && normalizedValue !== value) {
107+
headers.set(name, normalizedValue);
108+
}
109+
}
110+
103111
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
104112
(headers.get as typeof originalGet) = function (name) {
105113
const value = originalGet.call(headers, name);

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,49 @@ describe('Validation Utils', () => {
240240

241241
it('should allow accessing other headers without validation', () => {
242242
const req = new Request('http://example.com', {
243-
headers: { 'accept': 'application/json' },
243+
headers: { 'accept': 'text/html, application/json' },
244244
});
245245
const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts);
246246

247-
expect(secured.headers.get('accept')).toBe('application/json');
247+
expect(secured.headers.get('accept')).toBe('text/html, application/json');
248+
});
249+
250+
it('should return the validated host header token when accessed via get()', () => {
251+
const req = new Request('http://example.com', {
252+
headers: {
253+
host: 'example.com,@127.0.0.1:8765',
254+
'x-forwarded-host': 'sub.valid.com,@127.0.0.1:8765',
255+
},
256+
});
257+
const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts);
258+
259+
expect(secured.headers.get('host')).toBe('example.com');
260+
expect(secured.headers.get('x-forwarded-host')).toBe('sub.valid.com');
261+
});
262+
263+
it('should return validated host header tokens when iterating headers', () => {
264+
const req = new Request('http://example.com', {
265+
headers: {
266+
accept: 'text/html, application/json',
267+
host: 'example.com,@127.0.0.1:8765',
268+
'x-forwarded-host': 'sub.valid.com,@127.0.0.1:8765',
269+
},
270+
});
271+
const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts);
272+
273+
expect([...secured.headers.entries()]).toContain(['host', 'example.com']);
274+
expect([...secured.headers.entries()]).toContain(['x-forwarded-host', 'sub.valid.com']);
275+
expect([...secured.headers.values()]).toContain('example.com');
276+
expect([...secured.headers.values()]).toContain('sub.valid.com');
277+
expect([...secured.headers]).toContain(['host', 'example.com']);
278+
expect([...secured.headers]).toContain(['x-forwarded-host', 'sub.valid.com']);
279+
280+
const forEachValues = new Map<string, string>();
281+
secured.headers.forEach((value, key) => forEachValues.set(key, value));
282+
283+
expect(forEachValues.get('accept')).toBe('text/html, application/json');
284+
expect(forEachValues.get('host')).toBe('example.com');
285+
expect(forEachValues.get('x-forwarded-host')).toBe('sub.valid.com');
248286
});
249287

250288
it('should validate headers when iterating with entries()', async () => {

0 commit comments

Comments
 (0)