CVE-2026-95362
Overview
Background
- DevTools network resource loading
- the mechanism by which the DevTools front-end asks the browser process to fetch resources like source maps on its behalf via
DevToolsUIBindings::LoadNetworkResource. - `site_for_cookies`
- a field on
network::ResourceRequestthat identifies the top-level site for a request and governs whether same-site (non-SameSite=None) cookies are attached. - `request_initiator`
- a field on
network::ResourceRequestnaming the origin that initiated the request, used by the network service for CORS and other origin-based security decisions. - Cross-site request forgery (CSRF)
- an attack in which a request carries a victim’s ambient cookies to a target site, causing state-changing actions the victim did not intend.
Root Cause Analysis
When DevTools fetched a network resource in DevToolsUIBindings::LoadNetworkResource, it built a network::ResourceRequest whose site_for_cookies was derived directly from the resource URL itself via net::SiteForCookies::FromUrl(gurl), and it left request_initiator unset. Deriving site_for_cookies from the resource URL makes every such fetch appear first-party to its own target, so the network service attaches the victim’s same-site cookies to an arbitrary cross-site URL that a page could cause DevTools to load. This violated the invariant that a subresource fetch must carry the security context (initiator and first-party site) of the page that actually caused it — the inspected page — not the context of the URL being fetched.
The fix reads the inspected page’s primary main frame origin via GetPrimaryMainFrame()->GetLastCommittedOrigin(), sets request_initiator to that origin, and computes site_for_cookies with net::SiteForCookies::FromOrigin(inspected_origin), so the request is treated as originating from the inspected page and only carries cookies appropriate to that site.
The added test confirms site_for_cookies.IsFirstParty(resource_url) is now false while it is equivalent to the inspected origin’s site.
site_for_cookies to the inspected page’s committed origin instead.Attack Path
- Set up a malicious inspected page An attacker gets a victim to open DevTools on a page (or content) under attacker influence that can trigger a DevTools network resource load, such as via a crafted source map URL.
- Trigger a cross-site resource fetch
The page causes
DevToolsUIBindings::LoadNetworkResourceto fetch an arbitrary cross-site target URL, which builds anetwork::ResourceRequestfor that URL. - First-party context is forged
Because
site_for_cookieswas set from the target URL vianet::SiteForCookies::FromUrl(gurl)andrequest_initiatorwas unset, the network service treats the request as first-party to the target and attaches the victim’s same-site cookies. - Cross-site state change or data exposure The forged, cookie-bearing request reaches the target site as an authenticated CSRF request, potentially performing a state-changing action or exposing authenticated response data. (inferred)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/devtools/devtools_ui_bindings.cc |
modified | |
TEST_Fchrome/browser/devtools/devtools_ui_bindings_unittest.cc |
modified |
Files Changed
chrome/browser/devtools/devtools_ui_bindings_unittest.ccchrome/browser/devtools/devtools_ui_bindings.cc
Audit Directions
- Fetch-URL-derived first-party contextLook for other browser-process request builders that set
site_for_cookiesfrom the target URL vianet::SiteForCookies::FromUrlinstead of an inspected/initiating page origin. - Unset request_initiator on privileged fetchesAudit WebUI and DevTools-driven
network::ResourceRequestconstruction paths that omitrequest_initiator, since a missing initiator disables origin-based network-service protections. - Ambient-authority proxied requestsReview features where a privileged UI fetches URLs on behalf of a less-trusted page and confirm the resulting request inherits the page’s security context rather than the fetched resource’s.
Patch
From 6eb54c22b20c417dee1409564e8f2d635ff131a9 Mon Sep 17 00:00:00 2001 From: Alex Rudenko <[email protected]> Date: Fri, 28 Aug 2026 05:56:45 -0700 Subject: [PATCH] Use inspected page origin for DevTools network resource requests. When DevTools loads network resources (such as source maps), it now sets the request initiator and site-for-cookies based on the origin of the inspected page's primary main frame. Previously, site-for-cookies was derived from the resource URL itself, and the request initiator was not set. This change ensures proper cookie and security policy handling for these requests. Fixed: 511791538 Change-Id: I9d4f069f5022c62b50d755afe126d5d6fd5131c5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8310909 Auto-Submit: Alex Rudenko <[email protected]> Commit-Queue: Danil Somsikov <[email protected]> Reviewed-by: Danil Somsikov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1687931} --- diff --git a/chrome/browser/devtools/devtools_ui_bindings.cc b/chrome/browser/devtools/devtools_ui_bindings.cc index ccf1664d..c5c57ba 100644 --- a/chrome/browser/devtools/devtools_ui_bindings.cc +++ b/chrome/browser/devtools/devtools_ui_bindings.cc @@ -1306,12 +1306,16 @@ network::ResourceRequest resource_request; resource_request.url = gurl; - // TODO(caseq): this preserves behavior of URLFetcher-based implementation. - // We really need to pass proper first party origin from the front-end. - resource_request.site_for_cookies = net::SiteForCookies::FromUrl(gurl); resource_request.headers.AddHeadersFromString(headers); content::WebContents* target_tab = delegate_->GetInspectedWebContents(); + if (target_tab) { + const url::Origin& inspected_origin = + target_tab->GetPrimaryMainFrame()->GetLastCommittedOrigin(); + resource_request.request_initiator = inspected_origin; + resource_request.site_for_cookies = + net::SiteForCookies::FromOrigin(inspected_origin); + } NetworkResourceLoader::URLLoaderFactoryHolder url_loader_factory; if (gurl.SchemeIsFile()) { diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc index 7885666a..716d446 100644 --- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc +++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc @@ -31,10 +31,12 @@ #include "content/public/test/url_loader_interceptor.h" #include "google_apis/gaia/google_service_auth_error.h" #include "net/base/net_errors.h" +#include "net/cookies/site_for_cookies.h" #include "net/http/http_status_code.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" +#include "url/origin.h" using testing::_; @@ -234,6 +236,44 @@ } } +TEST_F(DevToolsUIBindingsLoadNetworkResourceTest, + UsesInspectedPageAsRequestInitiator) { + const GURL inspected_url("http://a.test/page.html"); + const GURL resource_url("http://b.test/source.map"); + + content::WebContents* inspected_web_contents = + web_contents_factory_.CreateWebContents(profile_.get()); + content::NavigationSimulator::NavigateAndCommitFromBrowser( + inspected_web_contents, inspected_url); + auto delegate = + std::make_unique<MockDevToolsUIBindingsDelegate>(inspected_web_contents); + bindings()->SetDelegate(delegate.release()); + + std::optional<network::ResourceRequest> captured_request; + content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting( + [&](content::URLLoaderInterceptor::RequestParams* params) { + captured_request = params->url_request; + content::URLLoaderInterceptor::WriteResponse( + "HTTP/1.1 200 OK\n\n", "{}", params->client.get()); + return true; + })); + + base::RunLoop run_loop; + CallLoadNetworkResource( + resource_url.spec(), "", 0, + base::BindLambdaForTesting( + [&](const base::Value*) { run_loop.Quit(); })); + run_loop.Run(); + + ASSERT_TRUE(captured_request.has_value()); + EXPECT_EQ(captured_request->url, resource_url); + const url::Origin inspected_origin = url::Origin::Create(inspected_url); + EXPECT_EQ(captured_request->request_initiator, inspected_origin); + EXPECT_TRUE(captured_request->site_for_cookies.IsEquivalent( + net::SiteForCookies::FromOrigin(inspected_origin))); + EXPECT_FALSE(captured_request->site_for_cookies.IsFirstParty(resource_url)); +} + TEST_F(DevToolsUIBindingsTest, SanitizeFrontendURL) { std::vector<std::pair<std::string, std::string>> tests = { {"random-string", "devtools://devtools/"},
Regression Test / PoC
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index 7885666a..716d446 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -31,10 +31,12 @@
#include "content/public/test/url_loader_interceptor.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/base/net_errors.h"
+#include "net/cookies/site_for_cookies.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "url/origin.h"
using testing::_;
@@ -234,6 +236,44 @@
}
}
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+ UsesInspectedPageAsRequestInitiator) {
+ const GURL inspected_url("http://a.test/page.html");
+ const GURL resource_url("http://b.test/source.map");
+
+ content::WebContents* inspected_web_contents =
+ web_contents_factory_.CreateWebContents(profile_.get());
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ inspected_web_contents, inspected_url);
+ auto delegate =
+ std::make_unique<MockDevToolsUIBindingsDelegate>(inspected_web_contents);
+ bindings()->SetDelegate(delegate.release());
+
+ std::optional<network::ResourceRequest> captured_request;
+ content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
+ [&](content::URLLoaderInterceptor::RequestParams* params) {
+ captured_request = params->url_request;
+ content::URLLoaderInterceptor::WriteResponse(
+ "HTTP/1.1 200 OK\n\n", "{}", params->client.get());
+ return true;
+ }));
+
+ base::RunLoop run_loop;
+ CallLoadNetworkResource(
+ resource_url.spec(), "", 0,
+ base::BindLambdaForTesting(
+ [&](const base::Value*) { run_loop.Quit(); }));
+ run_loop.Run();
+
+ ASSERT_TRUE(captured_request.has_value());
+ EXPECT_EQ(captured_request->url, resource_url);
+ const url::Origin inspected_origin = url::Origin::Create(inspected_url);
+ EXPECT_EQ(captured_request->request_initiator, inspected_origin);
+ EXPECT_TRUE(captured_request->site_for_cookies.IsEquivalent(
+ net::SiteForCookies::FromOrigin(inspected_origin)));
+ EXPECT_FALSE(captured_request->site_for_cookies.IsFirstParty(resource_url));
+}
+
TEST_F(DevToolsUIBindingsTest, SanitizeFrontendURL) {
std::vector<std::pair<std::string, std::string>> tests = {
{"random-string", "devtools://devtools/"},
Original Bug Report
SameSite=Strict CSRF and Fetch Metadata bypass in DevTools LoadNetworkResource
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: When DevTools falls back to host bindings to load network resources (like source maps for dedicated workers), it uses DevToolsUIBindings::LoadNetworkResource. This function issues the request using the browser process URL loader factory with an empty request_initiator and a site_for_cookies matching the target URL, causing the network stack to treat it as a trusted, browser-initiated request. This allows an attacker to perform credentialed GET-based CSRF attacks that bypass SameSite=Strict and Fetch Metadata protections.
Affected files:
chrome/browser/devtools/devtools_ui_bindings.cc
Estimated timestamp from git blame: 2019-12-15
Summary
A potential vulnerability in DevToolsUIBindings::LoadNetworkResource allows an attacker to trigger credentialed GET requests that bypass SameSite=Strict cookie restrictions and Fetch Metadata Resource Isolation Policies (Sec-Fetch-Site).
The issue arises because the browser process issues these requests with an empty request_initiator and a site_for_cookies matching the target URL, causing the network stack to treat them as browser-initiated (similar to a user typing a URL directly into the address bar). This behavior can be reliably triggered via source-map loading in dedicated workers.
Technical Details
The function DevToolsUIBindings::LoadNetworkResource in chrome/browser/devtools/devtools_ui_bindings.cc handles network resource loading for the DevTools frontend in cases where target-specific loading via CDP fails or is unavailable. When processing a request, it initializes a network::ResourceRequest with the following configuration:
- Site For Cookies:
resource_request.site_for_cookies = net::SiteForCookies::FromUrl(gurl);. This designates the request as first-party for the target URL. - Initiator: The
request_initiatorfield is left uninitialized, defaulting tostd::nullopt. - Credentials: The
credentials_modedefaults tomojom::CredentialsMode::kInclude. - Dispatch: The request is dispatched through
StoragePartitionImpl::GetURLLoaderFactoryForBrowserProcess().
In the Chromium network stack (//services/network/sec_header_helpers.cc), a request from the browser process with no initiator is assigned a SAME_SITE_STRICT cookie context and a Sec-Fetch-Site: none header. This allows the request to carry SameSite=Strict cookies and bypass security policies that trust the Sec-Fetch-Site: none value, which is intended only for browser-UI-initiated navigation or actions.
Reachability via Dedicated Workers
While DevTools typically loads resources via the inspected target’s network factory (which enforces isolation and CSP), dedicated workers currently fall back to this privileged browser-side binding:
- When the DevTools frontend attempts to load a source map for a dedicated worker via the standard
Network.loadNetworkResourceCDP command, it fails with a “Target not supported” error. This is becauseDedicatedWorkerDevToolsAgentHostdoes not provide the required network factory parameters, resulting in an invalid factory being generated in the browser process. - The DevTools frontend (
PageResourceLoader.ts) catches this protocol error. Because it is not a CSP violation, it ignores the error and falls back toloadFromHostBindings. loadFromHostBindingsuses the associated Mojo interfaceblink::mojom::DevToolsFrontendHostto invoke the vulnerableDevToolsUIBindings::LoadNetworkResourcebrowser-process logic.
This path also entirely bypasses the Content Security Policy (CSP) checks that are normally applied to frame-based resource loads in DevTools.
Suggested Exploitation Steps
(Note: These are potential steps based on code analysis; we do not have a working Proof of Concept yet).
- An attacker identifies a target GET action on
https://victim.example/actionprotected bySameSite=Strictcookies orSec-Fetch-Siterestrictions. - The attacker serves a page from
https://attacker.examplethat spawns a dedicated worker:new Worker('w.js');. - The worker script
w.jscontains a source map declaration pointing to the victim endpoint://# sourceMappingURL=https://victim.example/action. - A victim, logged into
https://victim.example, visitshttps://attacker.example. - The victim opens DevTools (e.g., F12).
- The DevTools frontend automatically attempts to fetch the source map for
w.js. - The network request to
https://victim.example/actionis issued by the browser process. It carriesSameSite=Strictcookies and theSec-Fetch-Site: noneheader, successfully bypassing cross-site protections and triggering the CSRF.
Suggested Fix
DevToolsUIBindings::LoadNetworkResource should not construct ResourceRequest objects that mimic browser-initiated navigation.
- Provide an Initiator: Set
request_initiatorto the origin of the page being inspected, or a specific DevTools origin, rather than leaving it asstd::nullopt. - Correct SiteForCookies: Do not blindly use
net::SiteForCookies::FromUrl(gurl). Thesite_for_cookiesshould reflect the context of the inspected page, not the requested resource URL. - Credentials: Re-evaluate if
mojom::CredentialsMode::kIncludeis appropriate for host-binding fallbacks, or if it should default tokOmitorkSameOrigin.
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.