Angular SSR SSRF Explained (CVE-2025-62427 and CVE-2026-27739)
Angular SSR applications had multiple reported SSRF vulnerabilities: CVE-2025-62427 (reporter - meDavidNS) and CVE-2026-27739 (reporter - Yenya030). This article explains how these vulnerabilities work and how Angular fixed them.
The issue occurs in Angular SSR's request URL construction logic. The createRequestUrl function builds the request URL by combining the request protocol, hostname, and request path, then converts the resulting string into a JavaScript URL object.
When Angular processes a relative request, it uses the host from the URL object created by createRequestUrl. However, if an attacker can manipulate the URL construction process and influence the host value of the generated URL object, a relative request can be resolved against an attacker-controlled host instead. This causes Angular SSR to send server-side requests to an unintended destination, resulting in server-side request forgery (SSRF).
CVE-2025-62427
The issue originates from how Angular constructs the request URL:
return new URL(originalUrl ?? url, `${protocol}://${hostnameWithPort}`);
According to the Node.js documentation:
new URL(input, base)
- If
inputis relative, it is resolved using thebase. - If
inputis absolute, thebaseis ignored.
For a normal relative path:
node -e "const u = new URL('/some-page', 'http://localhost:4200'); console.log(u.href)"
Result:
http://localhost:4200/some-page
The host remains localhost:4200 because the path is resolved against the base.
But there is a confusing case when the input is schema-relative. Although the input is considered relative, the base URL is not fully used. Only the scheme from the base URL is inherited, while the host is taken from the input.
node -e "const u = new URL('//some-page', 'http://localhost:4200'); console.log(u.href)"
Result:
http://some-page
Here, the host from the input (//some-page) overrides the base host.
Therefore, if a request is made like:
http://localhost:4200//attacker-domain.com/some-page
then:
input=//attacker-domain.com/some-pagebase=http://localhost:4200
The resulting URL becomes:
http://attacker-domain.com/some-page
Angular now treats attacker-domain.com as the origin.
CVE-2026-27739
After the previous vulnerability, Angular changed the code to:
return new URL(`${protocol}://${hostnameWithPort}${originalUrl ?? url}`);
Now, instead of providing separate input and base arguments to new URL(), Angular constructs the complete URL using string concatenation and then converts the resulting string into a URL object.
export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL {
const {
headers,
socket,
url = '',
originalUrl,
} = nodeRequest as IncomingMessage & { originalUrl?: string };
const protocol =
getFirstHeaderValue(headers['x-forwarded-proto']) ??
('encrypted' in socket && socket.encrypted ? 'https' : 'http');
const hostname =
getFirstHeaderValue(headers['x-forwarded-host']) ?? headers.host ?? headers[':authority'];
if (Array.isArray(hostname)) {
throw new Error('host value cannot be an array.');
}
let hostnameWithPort = hostname;
if (!hostname?.includes(':')) {
const port = getFirstHeaderValue(headers['x-forwarded-port']);
if (port) {
hostnameWithPort += `:${port}`;
}
}
return new URL(`${protocol}://${hostnameWithPort}${originalUrl ?? url}`);
}
However, the SSRF still possible because the protocol and hostnameWithPort values can be influenced by request headers such as:
X-Forwarded-HostX-Forwarded-ProtoHost
An attacker can manipulate these values and change the constructed URL's origin. This allows an attacker to redefine the application's base origin to an arbitrary external domain.
Similar to the previous vulnerability, when Angular’s HttpClient resolves relative URLs against this modified base origin, it sends the request to an attacker-controlled domain..