diff --git a/src/browser/WebSourceMapFetcher.ts b/src/browser/WebSourceMapFetcher.ts index 546fd9d97..3c2d3ae81 100644 --- a/src/browser/WebSourceMapFetcher.ts +++ b/src/browser/WebSourceMapFetcher.ts @@ -180,6 +180,15 @@ class SourceMapEncodingError extends Error { } } +class SourceMapRedirectLocationError extends Error { + constructor(url: string, location: string) { + super( + `Source-map redirect from ${JSON.stringify(url)} supplied an invalid Location URL: ${JSON.stringify(location)}.`, + ); + this.name = "SourceMapRedirectLocationError"; + } +} + const fetchOne = async ( request: WebSourceMapRequest, input: AnalyzeWebBundleInput, @@ -250,11 +259,11 @@ const fetchOne = async ( return emptySourceMapItem( request, "fetch_failed", - cause instanceof SourceMapSizeLimitError + cause instanceof SourceMapRedirectLocationError || + cause instanceof SourceMapSizeLimitError || + cause instanceof SourceMapDeadlineError ? cause.message - : cause instanceof SourceMapDeadlineError - ? cause.message - : "Source-map fetch or validation failed.", + : "Source-map fetch or validation failed.", ); } }; @@ -296,7 +305,11 @@ const fetchFollowingApprovedRedirects = async ( const location = response.headers.get("location"); if (location === null) return { response, fetchedUrl: current }; await response.body?.cancel(); - current = new URL(location, current).href; + try { + current = new URL(location, current).href; + } catch { + throw new SourceMapRedirectLocationError(current, location); + } } }; diff --git a/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts b/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts index 9a859f034..7131b30d8 100644 --- a/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts @@ -143,6 +143,7 @@ describe("CdpBrowserProvider: approved accessibility and source capture", () => items: [ { status: "included", + declared_url: `${browser.allowedOrigin}/app.js.map?token=map-secret`, original_sources: [ expect.objectContaining({ source: `${browser.allowedOrigin}/src/main.ts`, diff --git a/tests/boundary/browser/webSourceMapHttpStatus.test.ts b/tests/boundary/browser/webSourceMapHttpStatus.test.ts index 69763ddf9..f07419968 100644 --- a/tests/boundary/browser/webSourceMapHttpStatus.test.ts +++ b/tests/boundary/browser/webSourceMapHttpStatus.test.ts @@ -85,3 +85,41 @@ it.each([ } }, ); + +it("keeps the declared URL and reports malformed redirect Location evidence", async () => { + const declaredUrl = "https://app.example.test/declared.map"; + const input = analyzeWebBundleInputSchema.parse({ + cdp_endpoint: "http://127.0.0.1:9222", + target_id: "selected-page", + allowed_origins: ["https://app.example.test"], + fetch_source_maps: true, + }); + const result = await fetchWebSourceMaps( + [ + { + scriptKey: `scr_${"1".repeat(64)}`, + declaredUrl, + fetchUrl: declaredUrl, + }, + ], + input, + undefined, + { + fetch: () => + Promise.resolve( + new Response(null, { + status: 302, + headers: { location: "http://[invalid" }, + }), + ), + }, + ); + + expect(result.items[0]).toMatchObject({ + status: "fetch_failed", + declared_url: declaredUrl, + limitation: expect.stringContaining("invalid Location URL"), + }); + expect(result.items[0]?.limitation).toContain(declaredUrl); + expect(result.items[0]?.limitation).toContain("http://[invalid"); +});