CVE-2025-12445
Overview
Background
- Web-accessible resources
- the manifest list of files inside an extension that other origins may load through
chrome-extension://<id>/...URLs; anything not listed should be blocked to outside initiators. - `devtools_page`
- a manifest key that makes an extension a DevTools extension; its page is loaded inside the DevTools front end, which runs under the
devtools://scheme (content::kChromeDevToolsScheme). - Cross-renderer resource load check
- the browser-side logic in
chrome_url_request_util.ccthat decides whether a request for an extension resource coming from outside the extension is allowed, writing the answer through abool* allowedout-parameter. - `request_initiator`
- the origin that started a network request (
network::ResourceRequest::request_initiator); it is empty for browser-initiated requests.
Root Cause Analysis
The patched hunk is in a function in chrome/browser/extensions/chrome_url_request_util.cc whose name is not shown in the diff. The bool* allowed out-parameter suggests it is the cross-renderer resource-load check (inferred). After the explicit web-accessible-resource checks, the old code had a fallback. The removed comment states its intent: when an extension has no explicitly marked web-accessible resources, “the load should be allowed only if it is by DevTools”, approximated by “checking if the extension contains a DevTools page”. In practice, the code set *allowed = true for any extension with a non-empty chrome_manifest_urls::GetDevToolsPage(extension), without looking at who made the request. So every resource of a DevTools extension, including manifest.json in the regression test, was readable by any initiator that reached this check.
The fix keeps the DevTools-page condition but only allows the load when request.request_initiator is missing (browser-initiated) or its scheme is content::kChromeDevToolsScheme. Every other initiator now falls through to “couldn’t determine”, and the load is denied as the test expects (“Failed to fetch”).
The patch tightens the check.
devtools:// initiator or a browser-initiated request gets the exemption.Attack Path
- Target a DevTools extension
The victim has an installed extension that declares
devtools_pageand lists no web-accessible resources, so all of its files should be private. - Issue a cross-origin fetch
Another context, which in the regression test is a separate extension’s background service worker, calls
fetch('chrome-extension://<id>/manifest.json')against the DevTools extension. - Hit the overly broad fallback
The browser-side check finds no matching web-accessible resource but sees a non-empty
GetDevToolsPage(), and before the patch it set*allowed = truewithout checking the initiator. - Read private extension content The fetch succeeds and returns the file body, exposing resources the extension never made public. Fingerprinting the extension or reading its bundled code and data are possible uses (inferred).
Impact Assessment
devtools_page installed. The vendor rates this Low severity (“Policy bypass in Extensions”), and the provided metadata includes no exploited-in-the-wild statement.Files Changed
chrome/browser/extensions/api/devtools/devtools_apitest.ccchrome/browser/extensions/chrome_url_request_util.ccchrome/test/BUILD.gn
Audit Directions
- Capability-based approximations of requester identityLook for checks that grant access based on a property of the target (“extension has X page”, “origin has permission Y”) where the real intent concerns the requester, and confirm the initiator, process or origin is actually verified.
- Other fallbacks in extension resource gatingReview the remaining branches of the same function and of
extension_protocols.ccfor “allow by default” paths (component extensions, hosted apps, missing-initiator cases) that can be reached by unrelated extensions or web pages. - Empty-initiator trustThe fix still treats a missing
request_initiatoras browser-initiated. Audit whether any renderer- or extension-controlled path can produce requests without an initiator, such as navigations, service-worker-forwarded fetches or redirects, and so still hit the allow branch.
Patch
From 784d50d3c93d2f99ed5268a91fa94496bac7a665 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Mon, 08 Sep 2025 13:23:29 -0700
Subject: [PATCH] [Extensions] Fix devtools accesible resources issue
There's a bug where resources of devtools extensions are made
improperly available. Fix it, and add a regression test.
Bug: 428397712
Change-Id: Ib39ecbe64c571340ec365aa118faca43b643cd8c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6901326
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1512635}
---
diff --git a/chrome/browser/extensions/api/devtools/devtools_apitest.cc b/chrome/browser/extensions/api/devtools/devtools_apitest.cc
new file mode 100644
index 0000000..6c839513
--- /dev/null
+++ b/chrome/browser/extensions/api/devtools/devtools_apitest.cc
@@ -0,0 +1,78 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/strings/stringprintf.h"
+#include "base/values.h"
+#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/profiles/profile.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/background_script_executor.h"
+#include "extensions/test/test_extension_dir.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace extensions {
+
+using DevtoolsApiTest = ExtensionApiTest;
+
+// Tests that other extensions are not allowed to fetch resources of a devtools
+// extension that does not specify any web-accessible resources.
+// Regression test for https://crbug.com/428397712.
+IN_PROC_BROWSER_TEST_F(DevtoolsApiTest,
+ FetchBlockedWithoutWebAccessibleResources) {
+ // Load an extension that specifies a devtools page.
+ TestExtensionDir devtools_extension_dir;
+ devtools_extension_dir.WriteManifest(R"({
+ "name": "Devtools Extension",
+ "version": "1.0",
+ "manifest_version": 3,
+ "devtools_page": "devtools.html"
+ })");
+ devtools_extension_dir.WriteFile(FILE_PATH_LITERAL("devtools.html"), "");
+
+ const Extension* devtools_extension =
+ LoadExtension(devtools_extension_dir.UnpackedPath());
+ ASSERT_TRUE(devtools_extension);
+
+ // Load a second extension that will attempt to fetch content from the first.
+ TestExtensionDir fetching_extension_dir;
+ fetching_extension_dir.WriteManifest(R"({
+ "name": "Background Extension",
+ "version": "1.0",
+ "manifest_version": 3,
+ "background": {
+ "service_worker": "background.js"
+ }
+ })");
+ fetching_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");
+
+ const Extension* fetching_extension =
+ LoadExtension(fetching_extension_dir.UnpackedPath());
+ ASSERT_TRUE(fetching_extension);
+
+ // A script that will attempt to fetch the content of the manifest from the
+ // devtools extension.
+ std::string script = base::StringPrintf(
+ R"((async () => {
+ const url = 'chrome-extension://%s/manifest.json';
+ try {
+ const response = await fetch(url);
+ const manifestContent = await response.text();
+ chrome.test.sendScriptResult(manifestContent);
+ } catch (e) {
+ chrome.test.sendScriptResult(e.message);
+ }
+ })())",
+ devtools_extension->id().c_str());
+
+ BackgroundScriptExecutor executor(profile());
+ base::Value result = executor.ExecuteScript(
+ fetching_extension->id(), script,
+ BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
+
+ // The fetch should have failed.
+ ASSERT_TRUE(result.is_string());
+ EXPECT_THAT(result.GetString(), testing::HasSubstr("Failed to fetch"));
+}
+
+} // namespace extensions
diff --git a/chrome/browser/extensions/chrome_url_request_util.cc b/chrome/browser/extensions/chrome_url_request_util.cc
index 0f1884a..d5ad7c0 100644
--- a/chrome/browser/extensions/chrome_url_request_util.cc
+++ b/chrome/browser/extensions/chrome_url_request_util.cc
@@ -18,6 +18,7 @@
#include "base/strings/string_view_util.h"
#include "base/task/thread_pool.h"
#include "chrome/common/chrome_paths.h"
+#include "content/public/common/url_constants.h"
#include "extensions/browser/component_extension_resource_manager.h"
#include "extensions/browser/extension_protocols.h"
#include "extensions/browser/extensions_browser_client.h"
@@ -266,13 +267,16 @@
return true;
}
- // If there aren't any explicitly marked web accessible resources, the
- // load should be allowed only if it is by DevTools. A close approximation is
- // checking if the extension contains a DevTools page.
if (extension &&
!chrome_manifest_urls::GetDevToolsPage(extension).is_empty()) {
- *allowed = true;
- return true;
+ // Allow the load if the initiator is either a devtools origin, or if
+ // there is no initiator (in which case it was likely a browser-initiated
+ // request).
+ if (!request.request_initiator ||
+ request.request_initiator->scheme() == content::kChromeDevToolsScheme) {
+ *allowed = true;
+ return true;
+ }
}
// Couldn't determine if the resource is allowed or not.
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 9d59cad3..277716a6 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1558,6 +1558,7 @@
"../browser/extensions/api/declarative_net_request/declarative_net_request_apitest.cc",
"../browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc",
"../browser/extensions/api/developer_private/developer_private_apitest.cc",
+ "../browser/extensions/api/devtools/devtools_apitest.cc",
"../browser/extensions/api/dns/dns_apitest.cc",
"../browser/extensions/api/extension_action/extension_action_apitest.cc",
"../browser/extensions/api/extension_action/test_icon_image_observer.cc",
Regression Test / PoC
diff --git a/chrome/browser/extensions/api/devtools/devtools_apitest.cc b/chrome/browser/extensions/api/devtools/devtools_apitest.cc
new file mode 100644
index 0000000..6c839513
--- /dev/null
+++ b/chrome/browser/extensions/api/devtools/devtools_apitest.cc
@@ -0,0 +1,78 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/strings/stringprintf.h"
+#include "base/values.h"
+#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/profiles/profile.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/background_script_executor.h"
+#include "extensions/test/test_extension_dir.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace extensions {
+
+using DevtoolsApiTest = ExtensionApiTest;
+
+// Tests that other extensions are not allowed to fetch resources of a devtools
+// extension that does not specify any web-accessible resources.
+// Regression test for https://crbug.com/428397712.
+IN_PROC_BROWSER_TEST_F(DevtoolsApiTest,
+ FetchBlockedWithoutWebAccessibleResources) {
+ // Load an extension that specifies a devtools page.
+ TestExtensionDir devtools_extension_dir;
+ devtools_extension_dir.WriteManifest(R"({
+ "name": "Devtools Extension",
+ "version": "1.0",
+ "manifest_version": 3,
+ "devtools_page": "devtools.html"
+ })");
+ devtools_extension_dir.WriteFile(FILE_PATH_LITERAL("devtools.html"), "");
+
+ const Extension* devtools_extension =
+ LoadExtension(devtools_extension_dir.UnpackedPath());
+ ASSERT_TRUE(devtools_extension);
+
+ // Load a second extension that will attempt to fetch content from the first.
+ TestExtensionDir fetching_extension_dir;
+ fetching_extension_dir.WriteManifest(R"({
+ "name": "Background Extension",
+ "version": "1.0",
+ "manifest_version": 3,
+ "background": {
+ "service_worker": "background.js"
+ }
+ })");
+ fetching_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");
+
+ const Extension* fetching_extension =
+ LoadExtension(fetching_extension_dir.UnpackedPath());
+ ASSERT_TRUE(fetching_extension);
+
+ // A script that will attempt to fetch the content of the manifest from the
+ // devtools extension.
+ std::string script = base::StringPrintf(
+ R"((async () => {
+ const url = 'chrome-extension://%s/manifest.json';
+ try {
+ const response = await fetch(url);
+ const manifestContent = await response.text();
+ chrome.test.sendScriptResult(manifestContent);
+ } catch (e) {
+ chrome.test.sendScriptResult(e.message);
+ }
+ })())",
+ devtools_extension->id().c_str());
+
+ BackgroundScriptExecutor executor(profile());
+ base::Value result = executor.ExecuteScript(
+ fetching_extension->id(), script,
+ BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
+
+ // The fetch should have failed.
+ ASSERT_TRUE(result.is_string());
+ EXPECT_THAT(result.GetString(), testing::HasSubstr("Failed to fetch"));
+}
+
+} // namespace extensions
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 9d59cad3..277716a6 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1558,6 +1558,7 @@
"../browser/extensions/api/declarative_net_request/declarative_net_request_apitest.cc",
"../browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc",
"../browser/extensions/api/developer_private/developer_private_apitest.cc",
+ "../browser/extensions/api/devtools/devtools_apitest.cc",
"../browser/extensions/api/dns/dns_apitest.cc",
"../browser/extensions/api/extension_action/extension_action_apitest.cc",
"../browser/extensions/api/extension_action/test_icon_image_observer.cc",
Original Bug Report
Files of extensions with developer tools page are exposed to other extensions
Steps to reproduce the problem
- Download attached ZIP file and extract its contents.
- Install each of the three browser extensions that it contains (i.e. “actor”, “target-with-devtools” and “target-without-devtools”).
- Open JavaScript console of “actor” extension’s service worker.
- Run the following script (replace “TARGET WITH DEVTOOLS” and “TARGET WITHOUT DEVTOOLS” with the extension ID of the respective extension):
async function getFileContent(extensionId, filename) {
const resp = await fetch(`chrome-extension://${extensionId}/${filename}`);
return await resp.text();
}
await getFileContent("TARGET WITH DEVTOOLS", "manifest.json")
// '{\n "manifest_version": 3,\n "version": "0.1",\n "name": "Target (with developer tools)",\n "devtools_page": "devtools.html"\n}\n'
await getFileContent("TARGET WITH DEVTOOLS", "file.txt")
// 'Target extension file (with developer tools panel)\n'
await getFileContent("TARGET WITHOUT DEVTOOLS", "manifest.json")
// Uncaught TypeError: Failed to fetch
await getFileContent("TARGET WITHOUT DEVTOOLS", "file.txt")
// Uncaught TypeError: Failed to fetch
Problem Description
Any extension (despite it not having the “management” permission, or any other permissions) is able to read the contents of files contained within any extension that specifies the “devtools_page” key in its manifest (despite those files not being declared web-accessible). This allows the actor extension to (a) detect that the target extension is present, and (b) extract any potential secrets contained within the target extension.
Summary
Files of extensions with developer tools page are exposed to other extensions
Custom Questions
Reporter credit:
Thomas Greiner
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \