From 99ad47e58f1b3a5cbd18100734ea01787e6e4636 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:22:40 -0500 Subject: [PATCH 1/2] fix(service-worker): preserve referrer in asset requests Preserve referrer metadata when the service worker reconstructs asset requests for cache-busted and redirected asset fetches. For example, an attacker with access to asset host logs could receive a reset token embedded in a page URL if the reconstructed request falls back to default referrer behavior instead of carrying referrer: ''. --- packages/service-worker/worker/src/assets.ts | 9 ++--- .../service-worker/worker/test/happy_spec.ts | 36 +++++++++++++++++++ .../service-worker/worker/testing/fetch.ts | 6 +++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/service-worker/worker/src/assets.ts b/packages/service-worker/worker/src/assets.ts index cec901050b97..108bc284ac49 100644 --- a/packages/service-worker/worker/src/assets.ts +++ b/packages/service-worker/worker/src/assets.ts @@ -501,10 +501,10 @@ export abstract class AssetGroup { * Create a new `Request` based on the specified URL and `RequestInit` options, preserving only * metadata that are known to be safe. * - * Currently, headers, redirect policy, an explicit `credentials: 'omit'`, and the HTTP cache - * 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. + * Currently, headers, referrer, redirect policy, an explicit `credentials: 'omit'`, and the HTTP + * cache 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. @@ -532,6 +532,7 @@ export abstract class AssetGroup { const init: RequestInit = { headers, + referrer: options.referrer, redirect: options.redirect, }; diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index 2af08b1b8be5..1dc77a795619 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -1672,6 +1672,22 @@ import {envIsSupported} from '../testing/utils'; expect(bazReq.credentials).toBe('omit'); }); + it(`passes 'referrer' through to the server`, async () => { + const reqInit = {referrer: 'http://localhost/profile?token=secret'}; + expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz'); + + const [bazReq] = server.getRequestsFor('/baz.txt'); + expect(bazReq.referrer).toBe('http://localhost/profile?token=secret'); + }); + + it(`passes 'referrer: ""' through to the server`, async () => { + const reqInit = {referrer: ''}; + expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz'); + + const [bazReq] = server.getRequestsFor('/baz.txt'); + expect(bazReq.referrer).toBe(''); + }); + it(`passes 'cache' through to the server`, async () => { // Request a lazy-cached asset (so that it is fetched from the network) and provide an // explicit HTTP cache mode. @@ -1765,6 +1781,26 @@ import {envIsSupported} from '../testing/utils'; expect(redirectReq.credentials).toBe('omit'); }); + it(`passes 'referrer' through to the server`, async () => { + const reqInit = {referrer: 'http://localhost/profile?token=secret'}; + expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit)).toBe( + 'this was a redirect too', + ); + + const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt'); + expect(redirectReq.referrer).toBe('http://localhost/profile?token=secret'); + }); + + it(`passes 'referrer: ""' through to the server`, async () => { + const reqInit = {referrer: ''}; + expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit)).toBe( + 'this was a redirect too', + ); + + const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt'); + expect(redirectReq.referrer).toBe(''); + }); + it(`passes 'cache' through to the server`, async () => { // Request a redirected, lazy-cached asset (so that it is fetched from the network) and // provide an explicit HTTP cache mode. diff --git a/packages/service-worker/worker/testing/fetch.ts b/packages/service-worker/worker/testing/fetch.ts index 3e5997ba744c..7ff5a3bfa3b3 100644 --- a/packages/service-worker/worker/testing/fetch.ts +++ b/packages/service-worker/worker/testing/fetch.ts @@ -130,7 +130,7 @@ export class MockRequest extends MockBody implements Request { readonly method: string = 'GET'; readonly mode: RequestMode = 'cors'; readonly redirect: RequestRedirect = 'follow'; - readonly referrer: string = ''; + readonly referrer: string = 'about:client'; readonly referrerPolicy: ReferrerPolicy = 'no-referrer'; readonly signal: AbortSignal = null as any; @@ -163,6 +163,9 @@ export class MockRequest extends MockBody implements Request { if (init.redirect !== undefined) { this.redirect = init.redirect; } + if (init.referrer !== undefined) { + this.referrer = init.referrer; + } if (init.destination !== undefined) { this.destination = init.destination; } @@ -179,6 +182,7 @@ export class MockRequest extends MockBody implements Request { credentials: this.credentials, headers: this.headers, redirect: this.redirect, + referrer: this.referrer, }); } } From a7f52e5c3006f654fb5873c37fe5d9ede8548ae2 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:33:04 -0500 Subject: [PATCH 2/2] fix(service-worker): preserve referrer policy in asset requests Preserve explicit referrer policy when the service worker reconstructs asset requests for cache-busted and redirected asset fetches. For example, an application can load a script or image with referrerPolicy: 'same-origin' or 'origin' to limit referrer data. Dropping that policy can expose more of the current URL to that resource host. --- packages/service-worker/worker/src/assets.ts | 9 +++++---- .../service-worker/worker/test/happy_spec.ts | 18 ++++++++++++++++++ .../service-worker/worker/testing/fetch.ts | 6 +++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/service-worker/worker/src/assets.ts b/packages/service-worker/worker/src/assets.ts index 108bc284ac49..82b19de9433a 100644 --- a/packages/service-worker/worker/src/assets.ts +++ b/packages/service-worker/worker/src/assets.ts @@ -501,10 +501,10 @@ export abstract class AssetGroup { * Create a new `Request` based on the specified URL and `RequestInit` options, preserving only * metadata that are known to be safe. * - * Currently, headers, referrer, redirect policy, an explicit `credentials: 'omit'`, and the HTTP - * cache 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. + * Currently, headers, referrer, referrer policy, redirect policy, an explicit + * `credentials: 'omit'`, and the HTTP cache 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. @@ -533,6 +533,7 @@ export abstract class AssetGroup { const init: RequestInit = { headers, referrer: options.referrer, + referrerPolicy: options.referrerPolicy, redirect: options.redirect, }; diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index 1dc77a795619..9d2b6b480c40 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -1688,6 +1688,14 @@ import {envIsSupported} from '../testing/utils'; expect(bazReq.referrer).toBe(''); }); + it(`passes 'referrerPolicy' through to the server`, async () => { + const reqInit = {referrerPolicy: 'no-referrer'}; + expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz'); + + const [bazReq] = server.getRequestsFor('/baz.txt'); + expect(bazReq.referrerPolicy).toBe('no-referrer'); + }); + it(`passes 'cache' through to the server`, async () => { // Request a lazy-cached asset (so that it is fetched from the network) and provide an // explicit HTTP cache mode. @@ -1801,6 +1809,16 @@ import {envIsSupported} from '../testing/utils'; expect(redirectReq.referrer).toBe(''); }); + it(`passes 'referrerPolicy' through to the server`, async () => { + const reqInit = {referrerPolicy: 'no-referrer'}; + expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit)).toBe( + 'this was a redirect too', + ); + + const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt'); + expect(redirectReq.referrerPolicy).toBe('no-referrer'); + }); + it(`passes 'cache' through to the server`, async () => { // Request a redirected, lazy-cached asset (so that it is fetched from the network) and // provide an explicit HTTP cache mode. diff --git a/packages/service-worker/worker/testing/fetch.ts b/packages/service-worker/worker/testing/fetch.ts index 7ff5a3bfa3b3..0e6be61f6758 100644 --- a/packages/service-worker/worker/testing/fetch.ts +++ b/packages/service-worker/worker/testing/fetch.ts @@ -131,7 +131,7 @@ export class MockRequest extends MockBody implements Request { readonly mode: RequestMode = 'cors'; readonly redirect: RequestRedirect = 'follow'; readonly referrer: string = 'about:client'; - readonly referrerPolicy: ReferrerPolicy = 'no-referrer'; + readonly referrerPolicy: ReferrerPolicy = ''; readonly signal: AbortSignal = null as any; url: string; @@ -166,6 +166,9 @@ export class MockRequest extends MockBody implements Request { if (init.referrer !== undefined) { this.referrer = init.referrer; } + if (init.referrerPolicy !== undefined) { + this.referrerPolicy = init.referrerPolicy; + } if (init.destination !== undefined) { this.destination = init.destination; } @@ -183,6 +186,7 @@ export class MockRequest extends MockBody implements Request { headers: this.headers, redirect: this.redirect, referrer: this.referrer, + referrerPolicy: this.referrerPolicy, }); } }