Skip to content

fix(@angular/ssr): prevent open redirect via X-Forwarded-Prefix header #32521

Open
alan-agius4 wants to merge 3 commits intoangular:mainfrom
alan-agius4:security-validate-main-x-prefix
Open

fix(@angular/ssr): prevent open redirect via X-Forwarded-Prefix header #32521
alan-agius4 wants to merge 3 commits intoangular:mainfrom
alan-agius4:security-validate-main-x-prefix

Conversation

@alan-agius4
Copy link
Collaborator

@alan-agius4 alan-agius4 commented 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 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.

Closes #32501

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.
@alan-agius4 alan-agius4 force-pushed the security-validate-main-x-prefix branch from bdc4f23 to 33629cc Compare February 20, 2026 11:20
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
@alan-agius4 alan-agius4 force-pushed the security-validate-main-x-prefix branch from 33629cc to f181214 Compare February 20, 2026 13:50
@alan-agius4
Copy link
Collaborator Author

alan-agius4 commented Feb 20, 2026

@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.

@alan-agius4 alan-agius4 added action: review The PR is still awaiting reviews from at least one requested reviewer labels Feb 20, 2026
@alan-agius4 alan-agius4 marked this pull request as ready for review February 20, 2026 15:59
@@ -95,26 +95,32 @@ export function addTrailingSlash(url: string): string {
* ```
*/
export function joinUrlParts(...parts: string[]): string {
Copy link
Collaborator

@dgp1130 dgp1130 Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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('/'));

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 "\\".');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is there anything we can / should do for X-Forwarded-Prefix: ../? Is there any risk there?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: review The PR is still awaiting reviews from at least one requested reviewer area: @angular/ssr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open Redirect via Unsanitized X-Forwarded-Prefix Header, Chainable to Web Cache Poisoning

3 participants

Comments