Skip to content

Commit b90b9af

Browse files
committed
fix(@angular/ssr): decode x-forwarded-prefix before validation
The `x-forwarded-prefix` header can be percent-encoded. This change ensures that the header value is decoded before checking against the `INVALID_PREFIX_REGEX` to prevent bypassing security checks with encoded characters. Additionally, it adds error handling for malformed percent-encoding in the header.
1 parent 2678f5f commit b90b9af

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
@@ -268,9 +268,21 @@ function validateHeaders(request: Request): void {
268268
}
269269

270270
const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix'));
271-
if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) {
272-
throw new Error(
273-
'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.',
274-
);
271+
if (xForwardedPrefix) {
272+
let xForwardedPrefixDecoded: string;
273+
try {
274+
xForwardedPrefixDecoded = decodeURIComponent(xForwardedPrefix);
275+
} catch (e) {
276+
throw new Error(
277+
'Header "x-forwarded-prefix" contains an invalid value and cannot be decoded.',
278+
{ cause: e },
279+
);
280+
}
281+
282+
if (INVALID_PREFIX_REGEX.test(xForwardedPrefixDecoded)) {
283+
throw new Error(
284+
'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.',
285+
);
286+
}
275287
}
276288
}

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,17 @@ describe('Validation Utils', () => {
147147
);
148148
});
149149

150-
it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes', () => {
151-
const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil', '\\evil'];
150+
it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes including encoded', () => {
151+
const inputs = [
152+
'//evil',
153+
'\\\\evil',
154+
'/\\evil',
155+
'\\/evil',
156+
'\\evil',
157+
'%5Cevil',
158+
'%2F%2Fevil',
159+
'%2F..%2Fevil',
160+
];
152161

153162
for (const prefix of inputs) {
154163
const request = new Request('https://example.com', {
@@ -213,6 +222,18 @@ describe('Validation Utils', () => {
213222
.not.toThrow();
214223
}
215224
});
225+
226+
it('should throw error if x-forwarded-prefix contains malformed encoding', () => {
227+
const request = new Request('https://example.com', {
228+
headers: {
229+
'x-forwarded-prefix': '/%invalid',
230+
},
231+
});
232+
233+
expect(() => validateRequest(request, allowedHosts, false)).toThrowError(
234+
'Header "x-forwarded-prefix" contains an invalid value and cannot be decoded.',
235+
);
236+
});
216237
});
217238

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

0 commit comments

Comments
 (0)