VYPR
High severity8.3GHSA Advisory· Published Jun 15, 2026

@angular/service-worker: Sensitive Header Leakage on Cross-Origin Redirects in Angular Service Worker

CVE-2026-54264

Description

Angular Service Worker fails to strip sensitive headers on cross-origin redirects, allowing credential leakage to untrusted origins.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Angular Service Worker fails to strip sensitive headers on cross-origin redirects, allowing credential leakage to untrusted origins.

Vulnerability

An information disclosure vulnerability exists in the @angular/service-worker package (Angular framework). When the Service Worker fetches assets, it preserves request headers; however, on cross-origin redirects it fails to strip sensitive headers (Authorization, Proxy-Authorization, Cookie) as required by the Fetch redirect algorithm [1][2][3]. This affects all versions prior to the patched releases: 22.0.1, 21.2.17, 20.3.25 [2][3].

Exploitation

Exploitation requires the following conditions: the application uses @angular/service-worker, attaches sensitive headers (e.g., Authorization, Proxy-Authorization, or cookies) to asset-group requests, and one of those requests is redirected to a cross-origin, attacker-controlled domain [2]. The attacker must control or trigger the redirect endpoint. The Service Worker forwards the original request's headers to the new origin, as demonstrated in the fix's test which passes Authorization: Bearer secret to a different origin if not stripped [1]. No user interaction beyond normal browsing is needed.

Impact

An attacker who successfully receives the redirected request gains access to sensitive credentials such as Authorization tokens, Proxy-Authorization credentials, or session cookies. This can lead to full account takeover or session hijacking, as the credentials are sent to an untrusted third-party server [2]. The CVSS severity is high, reflecting the potential for credential disclosure.

Mitigation

The vulnerability has been patched in Angular versions 22.0.1, 21.2.17, and 20.3.25 [2][3]. Users should upgrade to these or later releases immediately. No workaround is provided by the vendor. The package is not known to be on the CISA KEV list. If upgrading is not immediately possible, avoid attaching credential headers to asset requests and ensure asset URLs do not undergo cross-origin redirects.

AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
47d68dcb2626

fix(service-worker): Strips sensitive headers on cross-origin redirects

https://github.com/angular/angularSkyZeroZxJun 2, 2026via body-scan-shorthand
5 files changed · +71 29
  • packages/service-worker/worker/src/adapter.ts+1 1 modified
    @@ -51,7 +51,7 @@ export class Adapter<T extends CacheStorage = CacheStorage> {
       /**
        * Wrapper around the `Headers` constructor.
        */
    -  newHeaders(headers: {[name: string]: string}): Headers {
    +  newHeaders(headers: HeadersInit): Headers {
         return new Headers(headers);
       }
     
    
  • packages/service-worker/worker/src/assets.ts+17 3 modified
    @@ -502,7 +502,9 @@ export abstract class AssetGroup {
        * metadata that are known to be safe.
        *
        * Currently, headers, redirect policy, an explicit `credentials: 'omit'`, and the HTTP cache
    -   * mode are preserved.
    +   * mode are preserved. On cross-origin redirects, sensitive headers are removed. This includes
    +   * `Authorization`, as required by the Fetch redirect algorithm, and forbidden request headers
    +   * that could contain credentials.
        *
        * NOTE:
        *   `credentials: 'same-origin'` and `credentials: 'include'` are intentionally not preserved.
    @@ -515,9 +517,21 @@ export abstract class AssetGroup {
        *   Investigate preserving more metadata. See, also, discussion on preserving `mode`:
        *   https://github.com/angular/angular/issues/41931#issuecomment-1227601347.
        */
    -  private newRequestWithMetadata(url: string, options: RequestInit): Request {
    +  private newRequestWithMetadata(url: string, options: Request): Request {
    +    let headers = options.headers;
    +    const parsedUrl = this.adapter.parseUrl(url, this.adapter.origin);
    +
    +    const hasHeaders = headers.keys().next().done !== true;
    +
    +    if (hasHeaders && parsedUrl.origin !== this.adapter.origin) {
    +      headers = this.adapter.newHeaders(options.headers);
    +      headers.delete('Authorization');
    +      headers.delete('Proxy-Authorization');
    +      headers.delete('Cookie');
    +    }
    +
         const init: RequestInit = {
    -      headers: options.headers,
    +      headers,
           redirect: options.redirect,
         };
     
    
  • packages/service-worker/worker/test/happy_spec.ts+33 2 modified
    @@ -135,7 +135,7 @@ import {envIsSupported} from '../testing/utils';
             name: 'other',
             installMode: 'lazy',
             updateMode: 'lazy',
    -        urls: ['/baz.txt', '/qux.txt', '/lazy/redirected.txt'],
    +        urls: ['/baz.txt', '/qux.txt', '/lazy/redirected.txt', '/lazy/cross-origin-redirected.txt'],
             patterns: [],
             cacheQueryOptions: {ignoreVary: true},
           },
    @@ -220,6 +220,11 @@ import {envIsSupported} from '../testing/utils';
         .withStaticFiles(dist)
         .withRedirect('/redirected.txt', '/redirect-target.txt')
         .withRedirect('/lazy/redirected.txt', '/lazy/redirect-target.txt')
    +    .withRedirect(
    +      '/lazy/cross-origin-redirected.txt',
    +      'https://example.com/lazy/redirect-target.txt',
    +    )
    +    .withRedirect('https://example.com/lazy/redirect-target.txt', '/lazy/redirect-target.txt')
         .withError('/error.txt');
     
       const server = serverBuilderBase.withManifest(manifest).build();
    @@ -1684,14 +1689,40 @@ import {envIsSupported} from '../testing/utils';
               // Request a redirected, lazy-cached asset (so that it is fetched from the network) and
               // provide headers.
               const reqInit = {
    -            headers: {SomeHeader: 'SomeValue'},
    +            headers: {
    +              Authorization: 'Bearer secret',
    +              SomeHeader: 'SomeValue',
    +            },
               };
               expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit)).toBe(
                 'this was a redirect too',
               );
     
               // Verify that the headers were passed through to the network.
               const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt');
    +          expect(redirectReq.headers.get('Authorization')).toBe('Bearer secret');
    +          expect(redirectReq.headers.get('SomeHeader')).toBe('SomeValue');
    +        });
    +
    +        it('does not pass sensitive headers through to a different origin', async () => {
    +          const reqInit = {
    +            headers: {
    +              Authorization: 'Bearer secret',
    +              Cookie: 'session=secret',
    +              'Proxy-Authorization': 'Basic secret',
    +              SomeHeader: 'SomeValue',
    +            },
    +          };
    +          expect(
    +            await makeRequest(scope, '/lazy/cross-origin-redirected.txt', undefined, reqInit),
    +          ).toBe('this was a redirect too');
    +
    +          const [redirectReq] = server.getRequestsFor(
    +            'https://example.com/lazy/redirect-target.txt',
    +          );
    +          expect(redirectReq.headers.get('Authorization')).toBeNull();
    +          expect(redirectReq.headers.get('Cookie')).toBeNull();
    +          expect(redirectReq.headers.get('Proxy-Authorization')).toBeNull();
               expect(redirectReq.headers.get('SomeHeader')).toBe('SomeValue');
             });
     
    
  • packages/service-worker/worker/testing/fetch.ts+18 18 modified
    @@ -59,6 +59,20 @@ export class MockBody implements Body {
     export class MockHeaders implements Headers {
       map = new Map<string, string>();
     
    +  constructor(headers?: HeadersInit) {
    +    if (headers === undefined) {
    +      return;
    +    }
    +
    +    if (Array.isArray(headers)) {
    +      headers.forEach(([name, value]) => this.set(name, value));
    +    } else if (headers instanceof MockHeaders || headers instanceof Headers) {
    +      headers.forEach((value, name) => this.set(name, value));
    +    } else {
    +      Object.entries(headers).forEach(([name, value]) => this.set(name, value));
    +    }
    +  }
    +
       [Symbol.iterator]() {
         return this.map[Symbol.iterator]();
       }
    @@ -131,15 +145,8 @@ export class MockRequest extends MockBody implements Request {
           throw 'Not implemented';
         }
         this.url = input;
    -    const headers = init.headers as {[key: string]: string};
    -    if (headers !== undefined) {
    -      if (headers instanceof MockHeaders) {
    -        this.headers = headers;
    -      } else {
    -        Object.keys(headers).forEach((header) => {
    -          this.headers.set(header, headers[header]);
    -        });
    -      }
    +    if (init.headers !== undefined) {
    +      this.headers = new MockHeaders(init.headers);
         }
         if (init.cache !== undefined) {
           this.cache = init.cache;
    @@ -195,15 +202,8 @@ export class MockResponse extends MockBody implements Response {
         super(typeof body === 'string' ? body : null);
         this.status = init.status !== undefined ? init.status : 200;
         this.statusText = init.statusText !== undefined ? init.statusText : 'OK';
    -    const headers = init.headers as {[key: string]: string};
    -    if (headers !== undefined) {
    -      if (headers instanceof MockHeaders) {
    -        this.headers = headers;
    -      } else {
    -        Object.keys(headers).forEach((header) => {
    -          this.headers.set(header, headers[header]);
    -        });
    -      }
    +    if (init.headers !== undefined) {
    +      this.headers = new MockHeaders(init.headers);
         }
         if (init.type !== undefined) {
           this.type = init.type;
    
  • packages/service-worker/worker/testing/scope.ts+2 5 modified
    @@ -181,11 +181,8 @@ export class SwTestHarnessImpl
         return new MockResponse(body, init);
       }
     
    -  override newHeaders(headers: {[name: string]: string}): Headers {
    -    return Object.keys(headers).reduce((mock, name) => {
    -      mock.set(name, headers[name]);
    -      return mock;
    -    }, new MockHeaders());
    +  override newHeaders(headers: HeadersInit): Headers {
    +    return new MockHeaders(headers);
       }
     
       async skipWaiting(): Promise<void> {
    

Vulnerability mechanics

Root cause

"The Angular Service Worker's `newRequestWithMetadata` method did not strip sensitive headers when following a cross-origin redirect, violating the Fetch redirect algorithm."

Attack vector

An attacker must first control a domain to which a credentialed asset request from an Angular Service Worker is redirected cross-origin. When the Service Worker follows the redirect, the vulnerable code previously forwarded sensitive headers (`Authorization`, `Cookie`, `Proxy-Authorization`) to the untrusted origin. The attacker can then harvest those credentials from the incoming request headers. This violates the Fetch API's redirect algorithm [ref_id=1].

Affected code

The vulnerability resides in `packages/service-worker/worker/src/assets.ts` within the `newRequestWithMetadata` method of the `AssetGroup` class. The patch also touches `packages/service-worker/worker/src/adapter.ts` and the test files `happy_spec.ts`, `fetch.ts`, and `scope.ts`.

What the fix does

The patch modifies `newRequestWithMetadata` in `assets.ts` to parse the redirect target's origin and compare it against the application's own origin. If the origins differ, it creates a fresh `Headers` object and explicitly deletes `Authorization`, `Proxy-Authorization`, and `Cookie` before constructing the redirected request. This aligns the Service Worker's behavior with the Fetch specification, preventing credential leakage to third-party origins [patch_id=6085145].

Preconditions

  • configThe application must use the `@angular/service-worker` package to fetch assets.
  • inputThe application must attach sensitive headers (e.g., Authorization, Cookie, Proxy-Authorization) to asset-group requests.
  • networkA credentialed asset request must encounter a cross-origin redirect to an attacker-controlled domain.

Generated on Jun 15, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.