fix(@angular/ssr): validate host headers to prevent header-based SSRF [main]#32499
fix(@angular/ssr): validate host headers to prevent header-based SSRF [main]#32499alan-agius4 wants to merge 2 commits intoangular:mainfrom
Conversation
07d009a to
26e57a6
Compare
26e57a6 to
40f0c57
Compare
ecd6230 to
1651898
Compare
1651898 to
46c57e1
Compare
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).
With this change:
- The `Host` and `X-Forwarded-Host` headers are validated against a strict allowlist.
- `localhost` and loopback addresses (e.g., `127.0.0.1`, `[::1]`) are allowed by default.
- `X-Forwarded-Port` must be numeric.
- `X-Forwarded-Proto` must be `http` or `https`.
- Requests with invalid or disallowed headers will now be rejected with a `400 Bad Request` status code.
BREAKING CHANGE:
Server-side requests will now fail with a `400 Bad Request` error if the `Host` header does not match a customized allowlist (or localhost).
**AngularAppEngine Users:**
To resolve this, you must configure the `allowedHosts` option in your `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"]
}
}
}
}
```
**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"]
});
```
46c57e1 to
3c7678c
Compare
dgp1130
left a comment
There was a problem hiding this comment.
Left a handful of suggestions. Don't worry about backporting anything not critical to your other PRs, I'm less concerned about maintainability, documentation, etc. aspects there, as long as main is in a good state going forward.
Also feel free to skip or come back to the less important comments in future PRs, no need to block this on more than we need to.
| export default { | ||
| basePath: '${basePath}', | ||
| allowedHosts: ${JSON.stringify( | ||
| allowedHosts.map((host) => host.replace(/^www\./i, '')), |
There was a problem hiding this comment.
Question: Do we also need to include loopback addresses like we have in CommonEngine, or is that already handled separately?
There was a problem hiding this comment.
It's already handled. (check the constructor)
| '\n\nAction Required: Update your "angular.json" to include this hostname. ' + | ||
| 'Path: "projects.[project-name].architect.build.options.security.allowedHosts".'; |
There was a problem hiding this comment.
Suggestion: This is implicitly stating that value is an acceptable host, but it may be correctly blocked as malicious and we wouldn't want to guide users to blindly allowing any hosts which try to connect.
We should suggest this only if value is an expected host. Maybe something like:
Header "
${headerName}" with value "${value}" is not allowed.Possible Action Required: If your server is intended to be hosted at "
${value}", update your "angular.json" to include this hostname. Path: "projects.[project-name].architect.build.options.security.allowedHosts". If your server is not hosted at "${value}", then this error can be safely ignored.
Also can we link to any docs for allowedHosts once that lands (maybe after this PR)?
| * @returns `true` if the hostname matches any of the wildcard hostnames, `false` otherwise. | ||
| */ | ||
| function checkWildcardHostnames(hostname: string, allowedHosts: ReadonlySet<string>): boolean { | ||
| for (const allowedHost of allowedHosts) { |
There was a problem hiding this comment.
Nitpicky perf thing: We're iterating over all of allowedHosts for every request. We could at least find all the wildcard hosts at startup and though only iterate through those for each request.
Probably small enough work being done here to be insignificant, up to you if it's worth the effort.
There was a problem hiding this comment.
I feel like this list of hosts would not be that large that this sort of micro optimization would have any benefit from.
bcfa19a to
14be1c9
Compare
14be1c9 to
60e7b81
Compare
This change introduces strict validation for
Host,X-Forwarded-Host,X-Forwarded-Proto, andX-Forwarded-Portheaders in the Angular SSR request handling pipeline, includingCommonEngineandAngularAppEngine.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
HttpClientrequests to arbitrary internal or external hosts (SSRF).With this change:
HostandX-Forwarded-Hostheaders are validated against a strict allowlist.localhostand loopback addresses (e.g.,127.0.0.1,[::1]) are allowed by default.X-Forwarded-Portmust be numeric.X-Forwarded-Protomust behttporhttps.400 Bad Requeststatus code.BREAKING CHANGE:
Server-side requests will now fail with a
400 Bad Requesterror if theHostheader does not match a customized allowlist (or localhost).AngularAppEngine Users:
To resolve this, you must configure the
allowedHostsoption in yourangular.jsonto include all domain names where your application is deployed.Example configuration in
angular.json:CommonEngine Users:
If you are using
CommonEngine, you must now provide theallowedHostsoption when initializing or rendering your application.Example: