Angular SSR Open Redirect (CVE-2026-27738 and CVE-2026-33397)

Introduction

In this year (2026) start, 3 security advisories was published for Angular SSR regarding an open redirect vulnerabilities.

Here are CVEs:

First 2 CVEs the same cause and second one is bypassing the earlier introduce validation mechanism. In this article we talk about the first 2 CVEs.

Before understanding how this vulnerability occurs, it is necessary to understand X-Forwarded-Prefix and protocol-relative requests.

Why X-Forwarded-Prefix

Sometimes you need to host multiple web applications on the same domain at different paths. For example, the main marketing pages at /, /contact, and /about serve one application, while /shop serves a different application, with /shop as its entry point.

In this scenario, a proxy can be configured to route all /shop/* requests to the second application after stripping the /shop prefix. As a result, the /shop application receives /shop as /, and /shop/cart as /cart.

Now consider an endpoint /shop/oldpage that should redirect to /shop/newpage. The server must return a redirect response with a Location header pointing to /shop/newpage. However, the application only sees the path as /oldpage, so it may generate a redirect to /newpage, which is incorrect.

In this setup, the proxy needs to be configured to include the X-Forwarded-Prefix header. This allows Angular to detect the original /shop prefix and correctly construct the Location header for the redirect response.

The problem is that an attacker can send a request with a malicious X-Forwarded-Prefix. Angular uses this value to define the Location header. For example, with X-Forwarded-Prefix: /shop, Angular sets the Location header value to /shop/newpage.

Protocol-relative request

When a redirect response contains //shop/newpage instead of a single leading slash, browsers interpret this as a protocol-relative URL, where the first path segment is treated as the hostname. As a result, the browser redirects to https://shop/newpage, leading to an open redirect.

X-Forwarded-Prefix: //shop does not directly generate //shop/newpage as the Location header value, but in some cases, double leading slashes in the resulting path cause the browser to interpret it as a protocol-relative request.

CVE-2026–27738

To understand why //shop/newpage is not produced for X-Forwarded-Prefix: //shop, consider the joinUrlParts() function in packages/angular/ssr/src/utils/url.ts, which is used to join the X-Forwarded-Prefix value.

export function joinUrlParts(...parts: string[]): string {
  const normalizeParts: string[] = [];
  for (const part of parts) {
    if (part === '') {
      // Skip any empty parts
      continue;
    }

    let normalizedPart = part;
    if (part[0] === '/') {
      normalizedPart = normalizedPart.slice(1);
    }
    if (part.at(-1) === '/') {
      normalizedPart = normalizedPart.slice(0, -1);
    }
    if (normalizedPart !== '') {
      normalizeParts.push(normalizedPart);
    }
  }

  return addLeadingSlash(normalizeParts.join('/'));
}

In joinUrlParts, each part is normalized by removing a single leading and trailing / without additional validation.

When the X-Forwarded-Prefix header contains multiple leading slashes, such as ///evil.com, only one leading slash is removed during normalization, with no further sanitization or validation applied.

Therefore, by using X-Forwarded-Prefix: ///evil.com, an attacker can perform an open redirect. It becomes more serious when the cache does not vary on the X-Forwarded-Prefix header. An attacker can poison cached redirect responses, causing users to be redirected to an attacker-controlled website.

Angular maintainers fixed the issue by:

  1. Updating joinUrlParts to strip all leading and trailing slashes from URL segments.
  2. Adding strict validation for the X-Forwarded-Prefix header using a regex.

CVE-2026–33397

As discussed earlier, protocol-relative requests are not limited to double leading slashes. Modern browsers may interpret the /\ sequence as //. If the Location header is /\evil.com/newpage, it can still trigger an open redirect and the same cache poisoning issue.

However, X-Forwarded-Prefix: /\evil.com does not work directly due to validation of the X-Forwarded-Prefix header:

const INVALID_PREFIX_REGEX = /^[/\\]{2}|(?:^|[/\\])\\.\\.?(?:[/\\]|$)/;

By looking at the updated joinUrlParts:

export function joinUrlParts(...parts: string[]): string {
  const normalizedParts: string[] = [];

  for (const part of parts) {
    if (part === '') {
      continue;
    }

    let start = 0;
    let end = part.length;

    // Use pointers to avoid intermediate slices
    while (start < end && part[start] === '/') {
      start++;
    }

    while (end > start && part[end - 1] === '/') {
      end--;
    }

    if (start < end) {
      normalizedParts.push(part.slice(start, end));
    }
  }

  return addLeadingSlash(normalizedParts.join('/'));
}

After normalization, a leading / is added. This means only \evil.com is needed as the value, and it is not caught by the regex.

As a result, using a value like \evil.com, an attacker can trigger an open redirect again.