Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in Extensions
DescriptionPolicy bypass in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker428397712
Fix commit784d50d3c93d (chromium/src) +88/-5
CISA KEVNot listed
CreditedThomas Greiner
Disclosed2025-10-28

Files Changed

  • chrome/browser/extensions/api/devtools/devtools_apitest.cc
  • chrome/browser/extensions/chrome_url_request_util.cc
  • chrome/test/BUILD.gn
From 784d50d3c93d2f99ed5268a91fa94496bac7a665 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <[email protected]>
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 <[email protected]>
Reviewed-by: Danil Somsikov <[email protected]>
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 [email protected]

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