fix(@angular/ssr): validate allowed host ports#33128
fix(@angular/ssr): validate allowed host ports#33128Hexix23 wants to merge 1 commit intoangular:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the host validation logic in the Angular SSR package to support port-specific allowlisting and improves terminology from 'hostnames' to 'hosts'. Key changes include refactoring isHostAllowed to accept a URL object and introducing isAllowedHostMatch for robust matching of wildcards and ports. A performance improvement was suggested for isHostAllowed to restore a fast path for exact host matches, avoiding unnecessary URL parsing during iteration.
| function isHostAllowed(url: URL, allowedHosts: ReadonlySet<string>): boolean { | ||
| if (allowedHosts.has('*')) { | ||
| return true; | ||
| } | ||
|
|
||
| for (const allowedHost of allowedHosts) { | ||
| if (!allowedHost.startsWith('*.')) { | ||
| continue; | ||
| } | ||
|
|
||
| const domain = allowedHost.slice(1); | ||
| if (hostname.endsWith(domain)) { | ||
| if (isAllowedHostMatch(url, allowedHost)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
The current implementation of isHostAllowed removes the fast path for exact matches that existed in the previous version. Since isHostAllowed is called for every request and iterates through the allowedHosts set—performing expensive URL parsing in each iteration via isAllowedHostMatch—adding back a fast path for exact matches of the normalized host (including the port) would significantly improve performance for the most common cases.
Using url.host in the has check is appropriate here as it includes the port for non-default values and excludes it for default ones, matching the expected normalization.
| function isHostAllowed(url: URL, allowedHosts: ReadonlySet<string>): boolean { | |
| if (allowedHosts.has('*')) { | |
| return true; | |
| } | |
| for (const allowedHost of allowedHosts) { | |
| if (!allowedHost.startsWith('*.')) { | |
| continue; | |
| } | |
| const domain = allowedHost.slice(1); | |
| if (hostname.endsWith(domain)) { | |
| if (isAllowedHostMatch(url, allowedHost)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function isHostAllowed(url: URL, allowedHosts: ReadonlySet<string>): boolean { | |
| if (allowedHosts.has('*') || allowedHosts.has(url.host)) { | |
| return true; | |
| } | |
| for (const allowedHost of allowedHosts) { | |
| if (isAllowedHostMatch(url, allowedHost)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } |
Require non-default ports to be explicitly allowed so forwarded host headers cannot retarget SSR requests to arbitrary services on an allowlisted hostname.
29f6f24 to
704d0fb
Compare
|
Validation is handled by the user when using the |
This fix addresses a Server-Side Request Forgery (SSRF) vulnerability in Angular SSR host validation where
allowedHostscompared only the URL hostname and ignored non-default ports.When
trustProxyHeadersallowsX-Forwarded-Host, an attacker could provide an allowlisted hostname with an arbitrary port, such asexample.com:9919. The previous validation accepted the header becauseURL.hostnamewas stillexample.com, while SSR relative requests could be resolved against the attacker-selected port.Changes:
allowedHosts.:80for HTTP and:443for HTTPS.hostname:portentries.*.domain:portentries.X-Forwarded-Hostvalidation.This prevents forwarded host headers from retargeting Angular SSR relative
HttpClientrequests to arbitrary services on an allowlisted hostname.PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
allowedHostsvalidation accepts an allowlisted hostname even when the effective URL contains an attacker-selected non-default port. For example,allowedHosts: ['example.com']allowsexample.com:9919because validation checks onlyURL.hostname.What is the new behavior?
Non-default ports must be explicitly allowed.
allowedHosts: ['example.com']still allowshttp://example.com,http://example.com:80,https://example.com, andhttps://example.com:443, but rejectsexample.com:9919. Applications that intentionally serve from a non-default port can useallowedHosts: ['example.com:9919'].Does this PR introduce a breaking change?
Other information
Security class: CWE-918 / Server-Side Request Forgery. This follows the public GitHub remediation path for an Angular SSR issue reported through Google OSS VRP.
Local validation run:
bazelisk test //packages/angular/ssr/test:test //packages/angular/ssr/node/test:test npx --yes prettier@3.6.2 --check packages/angular/ssr/src/utils/validation.ts packages/angular/ssr/test/utils/validation_spec.ts packages/angular/ssr/test/app-engine_spec.ts packages/angular/ssr/src/app-engine.ts packages/angular/ssr/src/manifest.ts git diff --check