Skip to content

Commit f100f2b

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 8dd341e commit f100f2b

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
@@ -281,9 +281,21 @@ function validateHeaders(request: Request): void {
281281
}
282282

283283
const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix'));
284-
if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) {
285-
throw new Error(
286-
'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.',
287-
);
284+
if (xForwardedPrefix) {
285+
let xForwardedPrefixDecoded: string;
286+
try {
287+
xForwardedPrefixDecoded = decodeURIComponent(xForwardedPrefix);
288+
} catch (e) {
289+
throw new Error(
290+
'Header "x-forwarded-prefix" contains an invalid value and cannot be decoded.',
291+
{ cause: e },
292+
);
293+
}
294+
295+
if (INVALID_PREFIX_REGEX.test(xForwardedPrefixDecoded)) {
296+
throw new Error(
297+
'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.',
298+
);
299+
}
288300
}
289301
}

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,17 @@ describe('Validation Utils', () => {
154154
);
155155
});
156156

157-
it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes', () => {
158-
const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil', '\\evil'];
157+
it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes including encoded', () => {
158+
const inputs = [
159+
'//evil',
160+
'\\\\evil',
161+
'/\\evil',
162+
'\\/evil',
163+
'\\evil',
164+
'%5Cevil',
165+
'%2F%2Fevil',
166+
'%2F..%2Fevil',
167+
];
159168

160169
for (const prefix of inputs) {
161170
const request = new Request('https://example.com', {
@@ -220,6 +229,18 @@ describe('Validation Utils', () => {
220229
.not.toThrow();
221230
}
222231
});
232+
233+
it('should throw error if x-forwarded-prefix contains malformed encoding', () => {
234+
const request = new Request('https://example.com', {
235+
headers: {
236+
'x-forwarded-prefix': '/%invalid',
237+
},
238+
});
239+
240+
expect(() => validateRequest(request, allowedHosts, false)).toThrowError(
241+
'Header "x-forwarded-prefix" contains an invalid value and cannot be decoded.',
242+
);
243+
});
223244
});
224245

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

0 commit comments

Comments
 (0)