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)

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:

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:

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..