VYPR
Medium severityNVD Advisory· Published Feb 25, 2026· Updated Apr 15, 2026

CVE-2026-27738

CVE-2026-27738

Description

The Angular SSR is a server-rise rendering tool for Angular applications. An Open Redirect vulnerability exists in the internal URL processing logic in versions on the 19.x branch prior to 19.2.21, the 20.x branch prior to 20.3.17, and the 21.x branch prior to 21.1.5 and 21.2.0-rc.1. The logic normalizes URL segments by stripping leading slashes; however, it only removes a single leading slash. When an Angular SSR application is deployed behind a proxy that passes the X-Forwarded-Prefix header, an attacker can provide a value starting with three slashes. This vulnerability allows attackers to conduct large-scale phishing and SEO hijacking. In order to be vulnerable, the application must use Angular SSR, the application must have routes that perform internal redirects, the infrastructure (Reverse Proxy/CDN) must pass the X-Forwarded-Prefix header to the SSR process without sanitization, and the cache must not vary on the X-Forwarded-Prefix header. Versions 21.2.0-rc.1, 21.1.5, 20.3.17, and 19.2.21 contain a patch. Until the patch is applied, developers should sanitize the X-Forwarded-Prefix header in theirserver.ts before the Angular engine processes the request.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@angular/ssrnpm
>= 21.2.0-next.0, < 21.2.0-rc.121.2.0-rc.1
@angular/ssrnpm
>= 21.0.0-next.0, < 21.1.521.1.5
@angular/ssrnpm
>= 20.0.0-next.0, < 20.3.1720.3.17
@angular/ssrnpm
>= 19.0.0-next.0, < 19.2.2119.2.21

Affected products

1

Patches

1
f086eccc36d1

fix(@angular/ssr): prevent open redirect via X-Forwarded-Prefix header

https://github.com/angular/angular-cliAlan AgiusFeb 23, 2026via ghsa
4 files changed · +106 9
  • packages/angular/ssr/src/utils/url.ts+15 9 modified
    @@ -95,26 +95,32 @@ export function addTrailingSlash(url: string): string {
      * ```
      */
     export function joinUrlParts(...parts: string[]): string {
    -  const normalizeParts: string[] = [];
    +  const normalizedParts: string[] = [];
    +
       for (const part of parts) {
         if (part === '') {
           // Skip any empty parts
           continue;
         }
     
    -    let normalizedPart = part;
    -    if (part[0] === '/') {
    -      normalizedPart = normalizedPart.slice(1);
    +    let start = 0;
    +    let end = part.length;
    +
    +    // Use "Pointers" to avoid intermediate slices
    +    while (start < end && part[start] === '/') {
    +      start++;
         }
    -    if (part.at(-1) === '/') {
    -      normalizedPart = normalizedPart.slice(0, -1);
    +
    +    while (end > start && part[end - 1] === '/') {
    +      end--;
         }
    -    if (normalizedPart !== '') {
    -      normalizeParts.push(normalizedPart);
    +
    +    if (start < end) {
    +      normalizedParts.push(part.slice(start, end));
         }
       }
     
    -  return addLeadingSlash(normalizeParts.join('/'));
    +  return addLeadingSlash(normalizedParts.join('/'));
     }
     
     /**
    
  • packages/angular/ssr/src/utils/validation.ts+12 0 modified
    @@ -26,6 +26,11 @@ const VALID_PROTO_REGEX = /^https?$/i;
      */
     const VALID_HOST_REGEX = /^[a-z0-9.:-]+$/i;
     
    +/**
    + * Regular expression to validate that the prefix is valid.
    + */
    +const INVALID_PREFIX_REGEX = /^[/\\]{2}|(?:^|[/\\])\.\.?(?:[/\\]|$)/;
    +
     /**
      * Extracts the first value from a multi-value header string.
      *
    @@ -253,4 +258,11 @@ function validateHeaders(request: Request): void {
       if (xForwardedProto && !VALID_PROTO_REGEX.test(xForwardedProto)) {
         throw new Error('Header "x-forwarded-proto" must be either "http" or "https".');
       }
    +
    +  const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix'));
    +  if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) {
    +    throw new Error(
    +      'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.',
    +    );
    +  }
     }
    
  • packages/angular/ssr/test/utils/url_spec.ts+12 0 modified
    @@ -100,6 +100,18 @@ describe('URL Utils', () => {
         it('should handle an all-empty URL parts', () => {
           expect(joinUrlParts('', '')).toBe('/');
         });
    +
    +    it('should normalize parts with multiple leading and trailing slashes', () => {
    +      expect(joinUrlParts('//path//', '///to///', '//resource//')).toBe('/path/to/resource');
    +    });
    +
    +    it('should handle a single part', () => {
    +      expect(joinUrlParts('path')).toBe('/path');
    +    });
    +
    +    it('should handle parts containing only slashes', () => {
    +      expect(joinUrlParts('//', '///')).toBe('/');
    +    });
       });
     
       describe('stripIndexHtmlFromURL', () => {
    
  • packages/angular/ssr/test/utils/validation_spec.ts+67 0 modified
    @@ -124,6 +124,73 @@ describe('Validation Utils', () => {
             'Header "x-forwarded-host" contains characters that are not allowed.',
           );
         });
    +
    +    it('should throw error if x-forwarded-prefix starts with multiple slashes or backslashes', () => {
    +      const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil'];
    +
    +      for (const prefix of inputs) {
    +        const request = new Request('https://example.com', {
    +          headers: {
    +            'x-forwarded-prefix': prefix,
    +          },
    +        });
    +
    +        expect(() => validateRequest(request, allowedHosts))
    +          .withContext(`Prefix: "${prefix}"`)
    +          .toThrowError(
    +            'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.',
    +          );
    +      }
    +    });
    +
    +    it('should throw error if x-forwarded-prefix contains dot segments', () => {
    +      const inputs = [
    +        '/./',
    +        '/../',
    +        '/foo/./bar',
    +        '/foo/../bar',
    +        '/.',
    +        '/..',
    +        './',
    +        '../',
    +        '.\\',
    +        '..\\',
    +        '/foo/.\\bar',
    +        '/foo/..\\bar',
    +        '.',
    +        '..',
    +      ];
    +
    +      for (const prefix of inputs) {
    +        const request = new Request('https://example.com', {
    +          headers: {
    +            'x-forwarded-prefix': prefix,
    +          },
    +        });
    +
    +        expect(() => validateRequest(request, allowedHosts))
    +          .withContext(`Prefix: "${prefix}"`)
    +          .toThrowError(
    +            'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.',
    +          );
    +      }
    +    });
    +
    +    it('should validate x-forwarded-prefix with valid dot usage', () => {
    +      const inputs = ['/foo.bar', '/foo.bar/baz', '/v1.2', '/.well-known'];
    +
    +      for (const prefix of inputs) {
    +        const request = new Request('https://example.com', {
    +          headers: {
    +            'x-forwarded-prefix': prefix,
    +          },
    +        });
    +
    +        expect(() => validateRequest(request, allowedHosts))
    +          .withContext(`Prefix: "${prefix}"`)
    +          .not.toThrow();
    +      }
    +    });
       });
     
       describe('cloneRequestAndPatchHeaders', () => {
    

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

7

News mentions

0

No linked articles in our index yet.