diff --git a/packages/service-worker/worker/src/assets.ts b/packages/service-worker/worker/src/assets.ts index 54fb4f66dc60..22ff7b898eca 100644 --- a/packages/service-worker/worker/src/assets.ts +++ b/packages/service-worker/worker/src/assets.ts @@ -363,7 +363,14 @@ export abstract class AssetGroup { } } - protected async fetchFromNetwork(req: Request, redirectLimit: number = 3): Promise { + protected async fetchFromNetwork( + req: Request, + redirectLimit: number = 3, + hashToVerify?: string, + ): Promise { + const originalUrl = this.adapter.normalizeUrl(req.url); + const canonicalHash = hashToVerify ?? this.hashes.get(originalUrl); + // Make a cache-busted request for the resource. const res = await this.cacheBustedFetchFromNetwork(req); @@ -377,7 +384,22 @@ export abstract class AssetGroup { } // Unwrap the redirect directly. - return this.fetchFromNetwork(this.newRequestWithMetadata(res.url, req), redirectLimit - 1); + const redirectedResponse = await this.fetchFromNetwork( + this.newRequestWithMetadata(res.url, req), + redirectLimit - 1, + canonicalHash, + ); + + if (canonicalHash !== undefined && redirectedResponse.ok) { + const redirectedHash = sha1Binary(await redirectedResponse.clone().arrayBuffer()); + if (canonicalHash !== redirectedHash) { + throw new SwCriticalError( + `Hash mismatch (fetchFromNetwork redirect): ${req.url}: expected ${canonicalHash}, got ${redirectedHash} after redirect to ${res.url}`, + ); + } + } + + return redirectedResponse; } return res; diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index 09b94ab4349e..e7721fa208b5 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -7,8 +7,10 @@ */ import {processNavigationUrls} from '../../config/src/generator'; +import {LazyAssetGroup} from '../src/assets'; import {CacheDatabase} from '../src/db-cache'; import {Driver, DriverReadyState} from '../src/driver'; +import {IdleScheduler} from '../src/idle'; import {Manifest} from '../src/manifest'; import {sha1} from '../src/sha1'; import {clearAllCaches, MockCache} from '../testing/cache'; @@ -378,6 +380,57 @@ import {envIsSupported} from '../testing/utils'; server.assertNoOtherRequests(); }); + it('rejects a re-fetched redirected hashed response with mismatched bytes', async () => { + class TestAssetGroup extends LazyAssetGroup { + fetchFromNetworkForTest(req: Request): Promise { + return this.fetchFromNetwork(req); + } + } + + const expectedBody = 'expected redirected body'; + const redirectedFs = new MockFileSystemBuilder() + .addFile('/redirect-target.txt', 'unexpected redirected body') + .build(); + const redirectedAssetGroup = { + name: 'assets', + installMode: 'lazy' as const, + updateMode: 'lazy' as const, + urls: ['/hashed-redirected.txt'], + patterns: [], + cacheQueryOptions: {ignoreVary: true}, + }; + const redirectedManifest: Manifest = { + configVersion: 1, + timestamp: 1234567890123, + index: '/hashed-redirected.txt', + assetGroups: [redirectedAssetGroup], + navigationUrls: [], + navigationRequestStrategy: 'performance', + hashTable: {'/hashed-redirected.txt': sha1(expectedBody)}, + }; + const redirectedServer = new MockServerStateBuilder() + .withStaticFiles(redirectedFs) + .withManifest(redirectedManifest) + .withRedirectedResponse('/hashed-redirected.txt', '/redirect-target.txt', expectedBody) + .build(); + const redirectedScope = new SwTestHarnessBuilder().withServerState(redirectedServer).build(); + const assetGroup = new TestAssetGroup( + redirectedScope, + redirectedScope, + new IdleScheduler(redirectedScope, 0, 0, {log: () => undefined}), + redirectedAssetGroup, + new Map([['/hashed-redirected.txt', sha1(expectedBody)]]), + new CacheDatabase(redirectedScope), + 'test', + ); + + await expectAsync( + assetGroup.fetchFromNetworkForTest(redirectedScope.newRequest('/hashed-redirected.txt')), + ).toBeRejectedWithError(/Hash mismatch \(fetchFromNetwork redirect\)/); + redirectedServer.assertSawRequestFor('/hashed-redirected.txt'); + redirectedServer.assertSawRequestFor('/redirect-target.txt'); + }); + it('caches lazy content on-request', async () => { expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); await driver.initialized; diff --git a/packages/service-worker/worker/testing/mock.ts b/packages/service-worker/worker/testing/mock.ts index f16d0ec2913c..86cb7c4327bd 100644 --- a/packages/service-worker/worker/testing/mock.ts +++ b/packages/service-worker/worker/testing/mock.ts @@ -115,6 +115,11 @@ export class MockServerStateBuilder { return this; } + withRedirectedResponse(from: string, to: string, body: string): MockServerStateBuilder { + this.resources.set(from, new MockResponse(body, {redirected: true, url: to})); + return this; + } + withError(url: string): MockServerStateBuilder { this.errors.add(url); return this;