You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Yes, this behavior used to work in the previous version
The previous version in which this bug was not present was
No response
Description
Description
Note
I have checked the official documentation, and there is no mention of allowedHosts equivalents or related configurations to prevent this in production SSR. If this behavior is already known or intentional, please feel free to close this report.
Important
While modern reverse proxies (e.g., Nginx, AWS ALB, Cloudflare) typically strip or override the X-Forwarded-Host header—mitigating direct host injection—they generally do not strip X-Forwarded-Port or X-Forwarded-Proto. This means an attacker can still exploit these unvalidated headers to poison CDN/proxy caches, even behind a properly configured reverse proxy.
createRequestUrl() in packages/angular/ssr/node/src/request.ts blindly trusts the X-Forwarded-Host, X-Forwarded-Proto, and X-Forwarded-Port headers when constructing the application's base URL during Server-Side Rendering. This allows an attacker to poison the DOCUMENT token and document.location object, leading to SSRF, Host Header Injection, SEO/Cache Poisoning, and Cache Poisoning DoS.
Note: This only affects the production server (node dist/.../server/server.mjs), NOT the dev server (ng serve). Vite's host-check-middleware correctly validates the Host header in dev mode.
What happens:
createRequestUrl() reads X-Forwarded-Host, X-Forwarded-Proto, and X-Forwarded-Port headers
It constructs a URL object from these values with no validation or allowlist check
This URL backs document.location in the SSR context
Any application code using document.location.origin (e.g., for API calls, canonical URLs, OAuth callbacks) uses the attacker-controlled value
Root cause (request.ts — createRequestUrl()):
constprotocol=getFirstHeaderValue(headers['x-forwarded-proto'])??('encrypted'insocket&&socket.encrypted ? 'https' : 'http');consthostname=getFirstHeaderValue(headers['x-forwarded-host'])??headers.host??headers[':authority'];lethostnameWithPort=hostname;if(!hostname?.includes(':')){constport=getFirstHeaderValue(headers['x-forwarded-port']);if(port){hostnameWithPort+=`:${port}`;}}// No validation of 'hostname' or 'protocol'returnnewURL(`${protocol}://${hostnameWithPort}${originalUrl??url}`);
getFirstHeaderValue() simply splits on , and returns the first trimmed value — it performs no validation or sanitization.
Data flow from header to application code:
createRequestUrl() → poisoned URL from X-Forwarded-* headers
createWebRequestFromNodeRequest() → Web Request with poisoned URL
AngularNodeAppEngine.handle() → passes to AngularAppEngine
AngularServerApp.handle() → extracts request.url, passes to renderAngular()
renderAngular() → injects as INITIAL_CONFIG.url into platformServer()
SSRF: App code like this.http.get(${doc.location.origin}/api/data) sends the request to the attacker's server, leaking cookies/tokens
SEO Poisoning:<link rel="canonical" href="${doc.location.href}"> renders with attacker's domain
Cache Poisoning DoS via X-Forwarded-Proto:curl -H "X-Forwarded-Proto: ftp" https://target.com/ → CDN caches HTML with ftp:// URLs → page breaks for all users
Cache Poisoning DoS via X-Forwarded-Port:curl -H "X-Forwarded-Port: 9999" https://target.com/ → CDN caches HTML with port 9999 URLs → assets fail to load for all users
Suggested fix:
Validate the host against a flexible allowlist (similar to the dev server's allowedHosts), or only trust X-Forwarded-* headers when explicit trust is configured (e.g., via a trustedProxies option in AngularNodeAppEngine).
Minimal Reproduction
Step 1: Create a new Angular SSR app
npx -y @angular/cli@latest new vuln-test-app --ssr --defaults
Step 2: Force runtime SSR in src/app/app.routes.server.ts (static prerendering masks the issue):
Expected behavior:
The server should either reject untrusted X-Forwarded-* headers or validate them against an allowlist. document.location.origin should reflect the actual server host (localhost:4000), not the spoofed header value.
Actual behavior:
The rendered HTML contains <h1>Origin: http://evil.com</h1>. The attacker fully controls document.location.origin during SSR via a single spoofed header.
No exception. The server responds normally with HTTP 200, but the SSR-rendered HTML contains attacker-controlled values in `document.location`, which poisons any application logic that relies on the origin:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
...<h1>Origin: http://evil.com</h1>...
The X-Forwarded-Host/Proto/Port headers are commonly set by reverse proxies (Nginx, HAProxy, AWS ALB, Kubernetes Ingress). Without validation, any client can spoof these headers when traffic reaches the Angular server directly or through a misconfigured proxy.
Cache Poisoning amplifies the impact: CDNs compute cache keys from Host + path but do NOT include X-Forwarded-Proto or X-Forwarded-Port. A single poisoned request can break the page for ALL subsequent users until the cache expires.
Temporary mitigation for developers: Add host validation middleware in server.ts before the Angular engine:
Command
build, other
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Description
Note
I have checked the official documentation, and there is no mention of
allowedHostsequivalents or related configurations to prevent this in production SSR. If this behavior is already known or intentional, please feel free to close this report.Important
While modern reverse proxies (e.g., Nginx, AWS ALB, Cloudflare) typically strip or override the
X-Forwarded-Hostheader—mitigating direct host injection—they generally do not stripX-Forwarded-PortorX-Forwarded-Proto. This means an attacker can still exploit these unvalidated headers to poison CDN/proxy caches, even behind a properly configured reverse proxy.createRequestUrl()inpackages/angular/ssr/node/src/request.tsblindly trusts theX-Forwarded-Host,X-Forwarded-Proto, andX-Forwarded-Portheaders when constructing the application's base URL during Server-Side Rendering. This allows an attacker to poison theDOCUMENTtoken anddocument.locationobject, leading to SSRF, Host Header Injection, SEO/Cache Poisoning, and Cache Poisoning DoS.What happens:
createRequestUrl()readsX-Forwarded-Host,X-Forwarded-Proto, andX-Forwarded-PortheadersURLobject from these values with no validation or allowlist checkdocument.locationin the SSR contextdocument.location.origin(e.g., for API calls, canonical URLs, OAuth callbacks) uses the attacker-controlled valueRoot cause (
request.ts—createRequestUrl()):getFirstHeaderValue()simply splits on,and returns the first trimmed value — it performs no validation or sanitization.Data flow from header to application code:
createRequestUrl()→ poisonedURLfromX-Forwarded-*headerscreateWebRequestFromNodeRequest()→ WebRequestwith poisoned URLAngularNodeAppEngine.handle()→ passes toAngularAppEngineAngularServerApp.handle()→ extractsrequest.url, passes torenderAngular()renderAngular()→ injects asINITIAL_CONFIG.urlintoplatformServer()ServerPlatformLocation→ backsdocument.locationdocument.location.origin→ attacker-controlledAttack scenarios:
this.http.get(${doc.location.origin}/api/data)sends the request to the attacker's server, leaking cookies/tokens<link rel="canonical" href="${doc.location.href}">renders with attacker's domainX-Forwarded-Proto:curl -H "X-Forwarded-Proto: ftp" https://target.com/→ CDN caches HTML withftp://URLs → page breaks for all usersX-Forwarded-Port:curl -H "X-Forwarded-Port: 9999" https://target.com/→ CDN caches HTML with port 9999 URLs → assets fail to load for all usersSuggested fix:
Validate the host against a flexible allowlist (similar to the dev server's
allowedHosts), or only trustX-Forwarded-*headers when explicit trust is configured (e.g., via atrustedProxiesoption inAngularNodeAppEngine).Minimal Reproduction
Step 1: Create a new Angular SSR app
Step 2: Force runtime SSR in
src/app/app.routes.server.ts(static prerendering masks the issue):Step 3: Instrument the component in
src/app/app.component.ts:Step 4: Build and run the production server:
Step 5: Send a request with a spoofed header:
curl -v -H "X-Forwarded-Host: evil.com" http://localhost:4000/Expected behavior:
The server should either reject untrusted
X-Forwarded-*headers or validate them against an allowlist.document.location.originshould reflect the actual server host (localhost:4000), not the spoofed header value.Actual behavior:
The rendered HTML contains
<h1>Origin: http://evil.com</h1>. The attacker fully controlsdocument.location.originduring SSR via a single spoofed header.Additional proof — protocol and port poisoning:
Exception or Error
Your Environment
Anything else relevant?
ng serve/ Vite) hashost-check-middlewarethat validates headers — it is NOT vulnerable.createRequestUrl()inpackages/angular/ssr/node/src/request.ts.X-Forwarded-Host/Proto/Portheaders are commonly set by reverse proxies (Nginx, HAProxy, AWS ALB, Kubernetes Ingress). Without validation, any client can spoof these headers when traffic reaches the Angular server directly or through a misconfigured proxy.Host+ path but do NOT includeX-Forwarded-ProtoorX-Forwarded-Port. A single poisoned request can break the page for ALL subsequent users until the cache expires.server.tsbefore the Angular engine: