fix(@angular/ssr): prevent open redirect via X-Forwarded-Prefix header #32521
Open
alan-agius4 wants to merge 3 commits intoangular:mainfrom
Open
fix(@angular/ssr): prevent open redirect via X-Forwarded-Prefix header #32521alan-agius4 wants to merge 3 commits intoangular:mainfrom
alan-agius4 wants to merge 3 commits intoangular:mainfrom
Conversation
This change introduces strict validation for `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-Port` headers in the Angular SSR request handling pipeline, including `CommonEngine` and `AngularAppEngine`.
Previously, the application engine constructed the base URL for server-side rendering using these headers without validation. This could allow an attacker to manipulate the headers to steer relative `HttpClient` requests to arbitrary internal or external hosts (SSRF).
The validation rules are:
- `Host` and `X-Forwarded-Host` headers are validated against a strict allowlist.
- `Host` and `X-Forwarded-Host` headers cannot contain path separators.
- `X-Forwarded-Port` header must be numeric.
- `X-Forwarded-Proto` header must be `http` or `https`.
Requests with invalid or disallowed headers will now log an error and fallback to Client-Side Rendering (CSR). In a future major version, these requests will be rejected with a 400 Bad Request.
Note: Most cloud providers and CDNs already validate these headers before the request reaches the text application, but this change adds an essential layer of defense-in-depth.
**AngularAppEngine Users:**
To exclude safe hosts from validation, configure the `allowedHosts` option in `angular.json` to include all domain names where your application is deployed.
Example configuration in `angular.json`:
```json
"architect": {
"build": {
"options": {
"security": {
"allowedHosts": ["example.com", "*.trusted-example.com"]
}
}
}
}
```
or
```ts
const appEngine = new AngularAppEngine({
allowedHosts: ["example.com", "*.trusted-example.com"]
})
const appEngine = new AngularNodeAppEngine({
allowedHosts: ["example.com", "*.trusted-example.com"]
})
```
**CommonEngine Users:**
If you are using `CommonEngine`, you must now provide the `allowedHosts` option when initializing or rendering your application.
Example:
```typescript
const commonEngine = new CommonEngine({
allowedHosts: ["example.com", "*.trusted-example.com"]
});
```
The application also respects `NG_ALLOWED_HOSTS` (comma-separated list) environment variable for authorizing hosts.
bdc4f23 to
33629cc
Compare
securityMB
reviewed
Feb 20, 2026
This change addresses a security vulnerability where `joinUrlParts()` in `packages/angular/ssr/src/utils/url.ts` only stripped one leading slash from URL parts. When the `X-Forwarded-Prefix` header contains multiple leading slashes (e.g., `///evil.com`), the function previously produced a protocol-relative URL (e.g., `//evil.com/home`). If the application issues a redirect (e.g., via a generic redirect route), the browser interprets this 'Location' header as an external redirect to `https://evil.com/home`. This vulnerability poses a significant risk as open redirects can be used in phishing attacks. Additionally, since the redirect response may lack `Cache-Control` headers, intermediate CDNs could cache the poisoned redirect, serving it to other users. This commit fixes the issue by: 1. Updating `joinUrlParts` to internally strip *all* leading and trailing slashes from URL segments, preventing the formation of protocol-relative URLs from malicious input. 2. Adding strict validation for the `X-Forwarded-Prefix` header to immediately reject requests with values starting with multiple slashes (`//`) or backslashes (`\\`). Closes angular#32501
33629cc to
f181214
Compare
Collaborator
Author
|
@dgp1130 & @AndrewKushnir please ignore the first commit as this is from #32516. @josephperrott, I added you by mistake as a reviewer, and I cannot remove you. |
dgp1130
approved these changes
Feb 20, 2026
| @@ -95,26 +95,32 @@ export function addTrailingSlash(url: string): string { | |||
| * ``` | |||
| */ | |||
| export function joinUrlParts(...parts: string[]): string { | |||
Collaborator
There was a problem hiding this comment.
Consider: Could a regex be simpler? Something like:
const regex = new Regex('^/*(?<part>.*?)/*$');
const normalizedParts = parts.map((part) => regex.exec(part).groups['part']);
return addLeadingSlash(normalizedParts.join('/'));
Collaborator
Author
There was a problem hiding this comment.
This is quite a hot function, whilst I didn’t benchmark it I assume the above is much slower.
|
|
||
| const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix')); | ||
| if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) { | ||
| throw new Error('Header "x-forwarded-prefix" must not start with multiple "/" or "\\".'); |
Collaborator
There was a problem hiding this comment.
Question: Is there anything we can / should do for X-Forwarded-Prefix: ../? Is there any risk there?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This change addresses a security vulnerability where
joinUrlParts()inpackages/angular/ssr/src/utils/url.tsonly stripped one leading slash from URL parts.When the
X-Forwarded-Prefixheader contains multiple leading slashes (e.g.,///evil.com), the function previously produced a protocol-relative URL (e.g.,//evil.com/home). If the application issues a redirect (e.g., via a generic redirect route), the browser interprets this 'Location' header as an external redirect tohttps://evil.com/home.This vulnerability poses a risk as open redirects can be used in phishing attacks. Additionally, since the redirect response may lack
Cache-Controlheaders, intermediate CDNs could cache the poisoned redirect, serving it to other users.This commit fixes the issue by:
joinUrlPartsto internally strip all leading and trailing slashes from URL segments, preventing the formation of protocol-relative URLs from malicious input.X-Forwarded-Prefixheader to immediately reject requests with values starting with multiple slashes.Closes #32501