Skip to content

fix(@angular/ssr): validate allowed host ports#33128

Closed
Hexix23 wants to merge 1 commit intoangular:mainfrom
Hexix23:fix-ssr-allowed-host-port
Closed

fix(@angular/ssr): validate allowed host ports#33128
Hexix23 wants to merge 1 commit intoangular:mainfrom
Hexix23:fix-ssr-allowed-host-port

Conversation

@Hexix23
Copy link
Copy Markdown

@Hexix23 Hexix23 commented May 5, 2026

This fix addresses a Server-Side Request Forgery (SSRF) vulnerability in Angular SSR host validation where allowedHosts compared only the URL hostname and ignored non-default ports.

When trustProxyHeaders allows X-Forwarded-Host, an attacker could provide an allowlisted hostname with an arbitrary port, such as example.com:9919. The previous validation accepted the header because URL.hostname was still example.com, while SSR relative requests could be resolved against the attacker-selected port.

Changes:

  • Validate the effective host, including non-default ports, against allowedHosts.
  • Continue to allow default ports such as :80 for HTTP and :443 for HTTPS.
  • Allow deployments that intentionally use non-default ports to opt in explicitly with hostname:port entries.
  • Preserve wildcard hostname matching and extend it to explicit *.domain:port entries.
  • Add regression coverage for URL validation and X-Forwarded-Host validation.

This prevents forwarded host headers from retargeting Angular SSR relative HttpClient requests 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?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

allowedHosts validation accepts an allowlisted hostname even when the effective URL contains an attacker-selected non-default port. For example, allowedHosts: ['example.com'] allows example.com:9919 because validation checks only URL.hostname.

What is the new behavior?

Non-default ports must be explicitly allowed. allowedHosts: ['example.com'] still allows http://example.com, http://example.com:80, https://example.com, and https://example.com:443, but rejects example.com:9919. Applications that intentionally serve from a non-default port can use allowedHosts: ['example.com:9919'].

Does this PR introduce a breaking change?

  • Yes
  • No

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

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +170 to 182
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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.
@Hexix23 Hexix23 force-pushed the fix-ssr-allowed-host-port branch from 29f6f24 to 704d0fb Compare May 5, 2026 11:03
@alan-agius4
Copy link
Copy Markdown
Collaborator

Validation is handled by the user when using the trustProxyHeaders configuration

@alan-agius4 alan-agius4 closed this May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants