CVE-2025-62427
Description
The Angular SSR is a server-rise rendering tool for Angular applications. The vulnerability is a Server-Side Request Forgery (SSRF) flaw within the URL resolution mechanism of Angular's Server-Side Rendering package (@angular/ssr) before 19.2.18, 20.3.6, and 21.0.0-next.8. The function createRequestUrl uses the native URL constructor. When an incoming request path (e.g., originalUrl or url) begins with a double forward slash (//) or backslash (\\), the URL constructor treats it as a schema-relative URL. This behavior overrides the security-intended base URL (protocol, host, and port) supplied as the second argument, instead resolving the URL against the scheme of the base URL but adopting the attacker-controlled hostname. This allows an attacker to specify an external domain in the URL path, tricking the Angular SSR environment into setting the page's virtual location (accessible via DOCUMENT or PlatformLocation tokens) to this attacker-controlled domain. Any subsequent relative HTTP requests made during the SSR process (e.g., using HttpClient.get('assets/data.json')) will be incorrectly resolved against the attacker's domain, forcing the server to communicate with an arbitrary external endpoint. This vulnerability is fixed in 19.2.18, 20.3.6, and 21.0.0-next.8.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@angular/ssrnpm | >= 19.0.0-next.0, < 19.2.18 | 19.2.18 |
@angular/ssrnpm | >= 20.0.0-next.0, < 20.3.6 | 20.3.6 |
@angular/ssrnpm | >= 21.0.0-next.0, < 21.0.0-next.8 | 21.0.0-next.8 |
Affected products
1- Range: 19.0.0-next.0, 19.0.0-next.1, 19.0.0-next.10, …
Patches
15271547c8066fix(@angular/ssr): prevent malicious URL from overriding host
2 files changed · +160 −2
packages/angular/ssr/node/src/request.ts+2 −2 modified@@ -76,7 +76,7 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers { * @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from. * @returns A `URL` object representing the request URL. */ -function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL { +export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL { const { headers, socket, @@ -101,7 +101,7 @@ function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): UR } } - return new URL(originalUrl ?? url, `${protocol}://${hostnameWithPort}`); + return new URL(`${protocol}://${hostnameWithPort}${originalUrl ?? url}`); } /**
packages/angular/ssr/node/test/request_spec.ts+158 −0 added@@ -0,0 +1,158 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { IncomingMessage } from 'node:http'; +import { Http2ServerRequest } from 'node:http2'; +import { Socket } from 'node:net'; +import { createRequestUrl } from '../src/request'; + +// Helper to create a mock request object for testing. +function createRequest(details: { + headers: Record<string, string | string[] | undefined>; + encryptedSocket?: boolean; + url?: string; + originalUrl?: string; +}): IncomingMessage { + return { + headers: details.headers, + socket: details.encryptedSocket ? ({ encrypted: true } as unknown as Socket) : new Socket(), + url: details.url, + originalUrl: details.originalUrl, + } as unknown as IncomingMessage; +} + +// Helper to create a mock Http2ServerRequest object for testing. +function createHttp2Request(details: { + headers: Record<string, string | string[] | undefined>; + url?: string; +}): Http2ServerRequest { + return { + headers: details.headers, + socket: new Socket(), + url: details.url, + } as Http2ServerRequest; +} + +describe('createRequestUrl', () => { + it('should create a http URL with hostname and port from the host header', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'localhost:8080' }, + url: '/test', + }), + ); + expect(url.href).toBe('http://localhost:8080/test'); + }); + + it('should create a https URL when the socket is encrypted', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '/test', + }), + ); + expect(url.href).toBe('https://example.com/test'); + }); + + it('should use "/" as the path when the URL path is empty', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '', + }), + ); + expect(url.href).toBe('https://example.com/'); + }); + + it('should preserve query parameters in the URL path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '/test?a=1', + }), + ); + expect(url.href).toBe('https://example.com/test?a=1'); + }); + + it('should prioritize "originalUrl" over "url" for the path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '/test', + originalUrl: '/original', + }), + ); + expect(url.href).toBe('https://example.com/original'); + }); + + it('should use "/" as the path when both "url" and "originalUrl" are not provided', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: undefined, + originalUrl: undefined, + }), + ); + expect(url.href).toBe('https://example.com/'); + }); + + it('should treat a protocol-relative value in "url" as part of the path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'localhost:8080' }, + url: '//example.com/test', + }), + ); + expect(url.href).toBe('http://localhost:8080//example.com/test'); + }); + + it('should treat a protocol-relative value in "originalUrl" as part of the path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'localhost:8080' }, + url: '/test', + originalUrl: '//example.com/original', + }), + ); + expect(url.href).toBe('http://localhost:8080//example.com/original'); + }); + + it('should prioritize "x-forwarded-host" and "x-forwarded-proto" headers', () => { + const url = createRequestUrl( + createRequest({ + headers: { + host: 'localhost:8080', + 'x-forwarded-host': 'example.com', + 'x-forwarded-proto': 'https', + }, + url: '/test', + }), + ); + expect(url.href).toBe('https://example.com/test'); + }); + + it('should use "x-forwarded-port" header for the port', () => { + const url = createRequestUrl( + createRequest({ + headers: { + host: 'localhost:8080', + 'x-forwarded-host': 'example.com', + 'x-forwarded-proto': 'https', + 'x-forwarded-port': '8443', + }, + url: '/test', + }), + ); + expect(url.href).toBe('https://example.com:8443/test'); + }); +});
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.