Skip to content

Commit 1dd1462

Browse files
committed
fix(@angular/ssr): decode x-forwarded-prefix before validation
The `x-forwarded-prefix` header can be percent-encoded. Validating it without decoding can allow bypassing security checks if subsequent processors (such as the `URL` constructor or a browser) implicitly decode it. Key bypass scenarios addressed: - **Implicit Decoding by URL Parsers**: A regex check for a literal `..` might miss `%2e%2e`. However, if the prefix is later passed to a `URL` constructor, it will treat `%2e%2e` as `..`, climbing up a directory. - **Browser Role in Redirects**: If an un-decoded encoded path is sent in a `Location` header, the browser will decode it, leading to unintended navigation. - **Double Slash Bypass**: Checking for a literal `//` misses `%2f%2f`. URL parsers might treat leading double slashes as protocol-relative URLs, leading to Open Redirects if interpreted as a hostname. This change ensures the validation "speaks the same language" as the URL parsing system by decoding the prefix before running safety checks. It also introduces robust handling for malformed percent-encoding.
1 parent e18c125 commit 1dd1462

File tree

2 files changed

+39
-6
lines changed

2 files changed

+39
-6
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,21 @@ function validateHeaders(request: Request): void {
260260
}
261261

262262
const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix'));
263-
if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) {
264-
throw new Error(
265-
'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.',
266-
);
263+
if (xForwardedPrefix) {
264+
let xForwardedPrefixDecoded: string;
265+
try {
266+
xForwardedPrefixDecoded = decodeURIComponent(xForwardedPrefix);
267+
} catch (e) {
268+
throw new Error(
269+
'Header "x-forwarded-prefix" contains an invalid value and cannot be decoded.',
270+
{ cause: e },
271+
);
272+
}
273+
274+
if (INVALID_PREFIX_REGEX.test(xForwardedPrefixDecoded)) {
275+
throw new Error(
276+
'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.',
277+
);
278+
}
267279
}
268280
}

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,17 @@ describe('Validation Utils', () => {
125125
);
126126
});
127127

128-
it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes', () => {
129-
const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil', '\\evil'];
128+
it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes including encoded', () => {
129+
const inputs = [
130+
'//evil',
131+
'\\\\evil',
132+
'/\\evil',
133+
'\\/evil',
134+
'\\evil',
135+
'%5Cevil',
136+
'%2F%2Fevil',
137+
'%2F..%2Fevil',
138+
];
130139

131140
for (const prefix of inputs) {
132141
const request = new Request('https://example.com', {
@@ -191,6 +200,18 @@ describe('Validation Utils', () => {
191200
.not.toThrow();
192201
}
193202
});
203+
204+
it('should throw error if x-forwarded-prefix contains malformed encoding', () => {
205+
const request = new Request('https://example.com', {
206+
headers: {
207+
'x-forwarded-prefix': '/%invalid',
208+
},
209+
});
210+
211+
expect(() => validateRequest(request, allowedHosts)).toThrowError(
212+
'Header "x-forwarded-prefix" contains an invalid value and cannot be decoded.',
213+
);
214+
});
194215
});
195216

196217
describe('cloneRequestAndPatchHeaders', () => {

0 commit comments

Comments
 (0)