Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions packages/angular/ssr/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@
*/

/**
* Common X-Forwarded-* headers.
* Internal sentinel string representing a wildcard rule to trust all proxy headers.
*/
const X_FORWARDED_HEADERS: ReadonlySet<string> = new Set([
'x-forwarded-for',
'x-forwarded-host',
'x-forwarded-port',
'x-forwarded-proto',
'x-forwarded-prefix',
]);
const TRUST_ALL_PROXY_HEADERS = '*';

/**
* The set of headers that should be validated for host header injection attacks.
Expand Down Expand Up @@ -235,7 +229,10 @@ export function isProxyHeaderAllowed(
headerName: string,
trustProxyHeaders: ReadonlySet<string>,
): boolean {
return trustProxyHeaders.has(headerName.toLowerCase());
return (
trustProxyHeaders.has(TRUST_ALL_PROXY_HEADERS) ||
trustProxyHeaders.has(headerName.toLowerCase())
);
}

/**
Expand All @@ -251,8 +248,15 @@ export function normalizeTrustProxyHeaders(
}

if (trustProxyHeaders === true) {
return X_FORWARDED_HEADERS;
return new Set([TRUST_ALL_PROXY_HEADERS]);
}

return new Set(trustProxyHeaders.map((h) => h.toLowerCase()));
const normalizedTrustedProxyHeaders = new Set(trustProxyHeaders.map((h) => h.toLowerCase()));
if (normalizedTrustedProxyHeaders.has(TRUST_ALL_PROXY_HEADERS)) {
throw new Error(
`"${TRUST_ALL_PROXY_HEADERS}" is not allowed as a value for the "trustProxyHeaders" option.`,
);
}

return normalizedTrustedProxyHeaders;
}
61 changes: 52 additions & 9 deletions packages/angular/ssr/test/utils/validation_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {
getFirstHeaderValue,
normalizeTrustProxyHeaders,
sanitizeRequestHeaders,
validateRequest,
validateUrl,
Expand Down Expand Up @@ -37,6 +38,35 @@ describe('Validation Utils', () => {
});
});

describe('normalizeTrustProxyHeaders', () => {
it('should return an empty set when input is undefined', () => {
expect(normalizeTrustProxyHeaders(undefined)).toEqual(new Set());
});

it('should return an empty set when input is false', () => {
expect(normalizeTrustProxyHeaders(false)).toEqual(new Set());
});

it('should return a set containing "*" when input is true', () => {
expect(normalizeTrustProxyHeaders(true)).toEqual(new Set(['*']));
});

it('should return a set of lowercased header names when input is an array of strings', () => {
expect(normalizeTrustProxyHeaders(['X-Forwarded-Host', 'X-Forwarded-Proto'])).toEqual(
new Set(['x-forwarded-host', 'x-forwarded-proto']),
);
});

it('should throw an error if input array contains "*"', () => {
expect(() => normalizeTrustProxyHeaders(['*'])).toThrowError(
'"*" is not allowed as a value for the "trustProxyHeaders" option.',
);
expect(() => normalizeTrustProxyHeaders(['X-Forwarded-Host', '*'])).toThrowError(
'"*" is not allowed as a value for the "trustProxyHeaders" option.',
);
});
});

describe('validateUrl', () => {
const allowedHosts = new Set(['example.com', '*.google.com']);

Expand Down Expand Up @@ -224,23 +254,37 @@ describe('Validation Utils', () => {
'x-forwarded-proto': 'https',
},
});
const secured = sanitizeRequestHeaders(req, new Set());
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined));

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

it('should retain allowed proxy headers when explicitly provided', () => {
const trustProxyHeaders = new Set(['x-forwarded-host']);
it('should scrub unallowed proxy headers when trustProxyHeaders is false', () => {
const req = new Request('http://example.com', {
headers: {
'host': 'example.com',
'x-forwarded-host': 'evil.com',
'x-forwarded-proto': 'https',
},
});
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(false));

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

it('should only retain allowed proxy headers when explicitly provided', () => {
const req = new Request('http://example.com', {
headers: {
'host': 'example.com',
'x-forwarded-host': 'proxy.com',
'x-forwarded-proto': 'https',
},
});
const secured = sanitizeRequestHeaders(req, trustProxyHeaders);
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(['x-forwarded-host']));

expect(secured.headers.get('host')).toBe('example.com');
expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com');
Expand All @@ -253,23 +297,22 @@ describe('Validation Utils', () => {
'host': 'example.com',
'x-forwarded-host': 'proxy.com',
'x-forwarded-proto': 'https',
'x-forwarded-email': 'user@example.com',
},
});
const secured = sanitizeRequestHeaders(
req,
new Set(['x-forwarded-host', 'x-forwarded-proto']),
);
const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(true));

expect(secured.headers.get('host')).toBe('example.com');
expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com');
expect(secured.headers.get('x-forwarded-proto')).toBe('https');
expect(secured.headers.get('x-forwarded-email')).toBe('user@example.com');
});

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

expect(secured).toBe(req);
expect(secured.headers.get('accept')).toBe('application/json');
Expand Down
Loading