Chrome · DevTools › Devtools
CVE-2026-106306
Authorization Error in DevTools (Devtools)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
DevToolsAllowedByCommandLineSwitchchrome/browser/devtools/devtools_browsertest.cc |
modified |
Files Changed
chrome/browser/devtools/devtools_browsertest.ccchrome/browser/devtools/devtools_availability_checker.cc
Patch
From 319af5bb13c8bfa9f7057f74f952217acf9c12af Mon Sep 17 00:00:00 2001
From: Jack Franklin <jacktfranklin@google.com>
Date: Tue, 01 Sep 2026 06:45:19 -0700
Subject: [PATCH] devtools: Enforce extension policy at target level for subframes
Bug: When an allowlisted page embeds a restricted extension subframe
(e.g. a force-installed enterprise extension):
* IsInspectionAllowed(DevToolsAgentHost*) fell back to the parent WebContents without checking extension policy on the subframe target.
* Because the parent page was allowlisted, the extension subframe target inherited allowlist status, bypassing the restricted extension policy.
Fix: Enforce extension policy directly in
IsInspectionAllowed(DevToolsAgentHost*) for chrome-extension:// targets
before falling back to WebContents. This blocks DevTools on restricted
extension subframe targets while leaving the allowlisted parent tab
inspectable.
Control flow in IsInspectionAllowed(Profile*, DevToolsAgentHost*):
Prior to this CL: 1. Check URL blocklist for agent_host->GetURL()
(passes, as the extension URL is not blocklisted). 2. Fall back to
IsInspectionAllowed(WebContents*) because no extension check existed on
agent_host.
* This is the bug: agent_host->GetWebContents() evaluates the parent tab's allowlisted URL, which early-returns true and incorrectly allows the extension subframe target.
With this CL: 1. Check URL blocklist for agent_host->GetURL(). 2. [NEW]
For chrome-extension:// targets, evaluate extension policy via
IsInspectionAllowed(Profile*, const Extension*) (returns false if
restricted). 3. Fall back to IsInspectionAllowed(WebContents*).
TAG=agy
CONV=3e995ef1-3928-4e76-b6a4-193b1360f070
Bug: 513383360
Change-Id: I7e74f5fde027bd7b7df49d73a3007eff7361c77f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8296678
Auto-Submit: Jack Franklin <jacktfranklin@chromium.org>
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Kateryna Prokopenko <kprokopenko@chromium.org>
Reviewed-by: Nikolay Vitkov <nvitkov@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1689811}
---
diff --git a/chrome/browser/devtools/devtools_availability_checker.cc b/chrome/browser/devtools/devtools_availability_checker.cc
index 752e82f..3e4d71a 100644
--- a/chrome/browser/devtools/devtools_availability_checker.cc
+++ b/chrome/browser/devtools/devtools_availability_checker.cc
@@ -93,6 +93,22 @@
return false;
}
+#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+ // Enforce extension policy at the target level before falling back to the
+ // parent WebContents.
+ if (target_url.SchemeIs(extensions::kExtensionScheme)) {
+ if (auto* registry = extensions::ExtensionRegistry::Get(profile)) {
+ if (const extensions::Extension* extension =
+ registry->GetInstalledExtension(
+ std::string(target_url.host()))) {
+ if (!IsInspectionAllowed(profile, extension)) {
+ return false;
+ }
+ }
+ }
+ }
+#endif
+
if (content::WebContents* web_contents = agent_host->GetWebContents()) {
return IsInspectionAllowed(profile, web_contents);
}
diff --git a/chrome/browser/devtools/devtools_browsertest.cc b/chrome/browser/devtools/devtools_browsertest.cc
index 7db1b09..f5298ac 100644
--- a/chrome/browser/devtools/devtools_browsertest.cc
+++ b/chrome/browser/devtools/devtools_browsertest.cc
@@ -3264,6 +3264,119 @@
auto agent_host = GetOrCreateDevToolsHostForWebContents(web_contents);
EXPECT_TRUE(DevToolsWindow::FindDevToolsWindow(agent_host.get()));
}
+
+IN_PROC_BROWSER_TEST_F(
+ DevToolsDisallowedForForceInstalledExtensionsPolicyTest,
+ AllowlistedMainFrameWithRestrictedExtensionIframeBlocked) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+ GURL allowed_url(embedded_test_server()->GetURL("/title1.html"));
+
+ base::ListValue allowlist;
+ allowlist.Append(allowed_url.spec());
+ profile()->GetPrefs()->SetList(prefs::kDeveloperToolsAvailabilityAllowlist,
+ std::move(allowlist));
+
+ base::FilePath extension_path =
+ test_data_dir_.AppendASCII("web_accessible_resources")
+ .AppendASCII("subframe");
+ const Extension* extension = InstallExtension(
+ extension_path, 1, ManifestLocation::kExternalPolicyDownload);
+ ASSERT_TRUE(extension);
+ std::string extension_id = extension->id();
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), allowed_url));
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ GURL extension_url("chrome-extension://" + extension_id +
+ "/web_accessible_page.html");
+ content::TestNavigationObserver nav_observer(extension_url);
+ nav_observer.WatchExistingWebContents();
+ ASSERT_TRUE(content::ExecJs(
+ web_contents->GetPrimaryMainFrame(),
+ content::JsReplace("let iframe = document.createElement('iframe');"
+ "iframe.src = $1;"
+ "document.body.appendChild(iframe);",
+ extension_url)));
+ nav_observer.Wait();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ DevToolsWindow::OpenDevToolsWindow(web_contents,
+ DevToolsOpenedByAction::kUnknown);
+ auto agent_host = GetOrCreateDevToolsHostForWebContents(web_contents);
+ EXPECT_TRUE(DevToolsWindow::FindDevToolsWindow(agent_host.get()));
+
+ scoped_refptr<content::DevToolsAgentHost> subframe_agent_host;
+ for (const auto& host : content::DevToolsAgentHost::GetOrCreateAll()) {
+ if (host->GetURL() == extension_url) {
+ subframe_agent_host = host;
+ break;
+ }
+ }
+ ASSERT_TRUE(subframe_agent_host);
+ EXPECT_FALSE(IsInspectionAllowed(profile(), subframe_agent_host.get()));
+}
+
+IN_PROC_BROWSER_TEST_F(
+ DevToolsDisallowedForForceInstalledExtensionsPolicyTest,
+ AllowlistedMainFrameWithRestrictedExtensionIframeAllowedWhenPolicyAllowed) {
+ AllowDevTools(browser_window_interface());
+
+ ASSERT_TRUE(embedded_test_server()->Start());
+ GURL allowed_url(embedded_test_server()->GetURL("/title1.html"));
+
+ base::ListValue allowlist;
+ allowlist.Append("chrome-extension://*");
+ allowlist.Append(allowed_url.spec());
+ profile()->GetPrefs()->SetList(prefs::kDeveloperToolsAvailabilityAllowlist,
+ std::move(allowlist));
+
+ base::FilePath extension_path =
+ test_data_dir_.AppendASCII("web_accessible_resources")
+ .AppendASCII("subframe");
+ const Extension* extension = InstallExtension(
+ extension_path, 1, ManifestLocation::kExternalPolicyDownload);
+ ASSERT_TRUE(extension);
+ std::string extension_id = extension->id();
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), allowed_url));
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ GURL extension_url("chrome-extension://" + extension_id +
+ "/web_accessible_page.html");
+ content::TestNavigationObserver nav_observer(extension_url);
+ nav_observer.WatchExistingWebContents();
+ ASSERT_TRUE(content::ExecJs(
+ web_contents->GetPrimaryMainFrame(),
+ content::JsReplace("let iframe = document.createElement('iframe');"
+ "iframe.src = $1;"
+ "document.body.appendChild(iframe);",
+ extension_url)));
+ nav_observer.Wait();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ DevToolsWindow::OpenDevToolsWindow(web_contents,
+ DevToolsOpenedByAction::kUnknown);
+ auto agent_host = GetOrCreateDevToolsHostForWebContents(web_contents);
+ EXPECT_TRUE(DevToolsWindow::FindDevToolsWindow(agent_host.get()));
+
+ scoped_refptr<content::DevToolsAgentHost> subframe_agent_host;
+ for (const auto& host : content::DevToolsAgentHost::GetOrCreateAll()) {
+ if (host->GetURL() == extension_url) {
+ subframe_agent_host = host;
+ break;
+ }
+ }
+ ASSERT_TRUE(subframe_agent_host);
+ EXPECT_TRUE(IsInspectionAllowed(profile(), subframe_agent_host.get()));
+}
#endif // !BUILDFLAG(IS_ANDROID)
class DevToolsAllowedByCommandLineSwitch
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/devtools/devtools_browsertest.cc b/chrome/browser/devtools/devtools_browsertest.cc
index 7db1b09..f5298ac 100644
--- a/chrome/browser/devtools/devtools_browsertest.cc
+++ b/chrome/browser/devtools/devtools_browsertest.cc
@@ -3264,6 +3264,119 @@
auto agent_host = GetOrCreateDevToolsHostForWebContents(web_contents);
EXPECT_TRUE(DevToolsWindow::FindDevToolsWindow(agent_host.get()));
}
+
+IN_PROC_BROWSER_TEST_F(
+ DevToolsDisallowedForForceInstalledExtensionsPolicyTest,
+ AllowlistedMainFrameWithRestrictedExtensionIframeBlocked) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+ GURL allowed_url(embedded_test_server()->GetURL("/title1.html"));
+
+ base::ListValue allowlist;
+ allowlist.Append(allowed_url.spec());
+ profile()->GetPrefs()->SetList(prefs::kDeveloperToolsAvailabilityAllowlist,
+ std::move(allowlist));
+
+ base::FilePath extension_path =
+ test_data_dir_.AppendASCII("web_accessible_resources")
+ .AppendASCII("subframe");
+ const Extension* extension = InstallExtension(
+ extension_path, 1, ManifestLocation::kExternalPolicyDownload);
+ ASSERT_TRUE(extension);
+ std::string extension_id = extension->id();
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), allowed_url));
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ GURL extension_url("chrome-extension://" + extension_id +
+ "/web_accessible_page.html");
+ content::TestNavigationObserver nav_observer(extension_url);
+ nav_observer.WatchExistingWebContents();
+ ASSERT_TRUE(content::ExecJs(
+ web_contents->GetPrimaryMainFrame(),
+ content::JsReplace("let iframe = document.createElement('iframe');"
+ "iframe.src = $1;"
+ "document.body.appendChild(iframe);",
+ extension_url)));
+ nav_observer.Wait();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ DevToolsWindow::OpenDevToolsWindow(web_contents,
+ DevToolsOpenedByAction::kUnknown);
+ auto agent_host = GetOrCreateDevToolsHostForWebContents(web_contents);
+ EXPECT_TRUE(DevToolsWindow::FindDevToolsWindow(agent_host.get()));
+
+ scoped_refptr<content::DevToolsAgentHost> subframe_agent_host;
+ for (const auto& host : content::DevToolsAgentHost::GetOrCreateAll()) {
+ if (host->GetURL() == extension_url) {
+ subframe_agent_host = host;
+ break;
+ }
+ }
+ ASSERT_TRUE(subframe_agent_host);
+ EXPECT_FALSE(IsInspectionAllowed(profile(), subframe_agent_host.get()));
+}
+
+IN_PROC_BROWSER_TEST_F(
+ DevToolsDisallowedForForceInstalledExtensionsPolicyTest,
+ AllowlistedMainFrameWithRestrictedExtensionIframeAllowedWhenPolicyAllowed) {
+ AllowDevTools(browser_window_interface());
+
+ ASSERT_TRUE(embedded_test_server()->Start());
+ GURL allowed_url(embedded_test_server()->GetURL("/title1.html"));
+
+ base::ListValue allowlist;
+ allowlist.Append("chrome-extension://*");
+ allowlist.Append(allowed_url.spec());
+ profile()->GetPrefs()->SetList(prefs::kDeveloperToolsAvailabilityAllowlist,
+ std::move(allowlist));
+
+ base::FilePath extension_path =
+ test_data_dir_.AppendASCII("web_accessible_resources")
+ .AppendASCII("subframe");
+ const Extension* extension = InstallExtension(
+ extension_path, 1, ManifestLocation::kExternalPolicyDownload);
+ ASSERT_TRUE(extension);
+ std::string extension_id = extension->id();
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), allowed_url));
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ GURL extension_url("chrome-extension://" + extension_id +
+ "/web_accessible_page.html");
+ content::TestNavigationObserver nav_observer(extension_url);
+ nav_observer.WatchExistingWebContents();
+ ASSERT_TRUE(content::ExecJs(
+ web_contents->GetPrimaryMainFrame(),
+ content::JsReplace("let iframe = document.createElement('iframe');"
+ "iframe.src = $1;"
+ "document.body.appendChild(iframe);",
+ extension_url)));
+ nav_observer.Wait();
+
+ EXPECT_TRUE(DevToolsWindow::AllowDevToolsFor(profile(), web_contents));
+
+ DevToolsWindow::OpenDevToolsWindow(web_contents,
+ DevToolsOpenedByAction::kUnknown);
+ auto agent_host = GetOrCreateDevToolsHostForWebContents(web_contents);
+ EXPECT_TRUE(DevToolsWindow::FindDevToolsWindow(agent_host.get()));
+
+ scoped_refptr<content::DevToolsAgentHost> subframe_agent_host;
+ for (const auto& host : content::DevToolsAgentHost::GetOrCreateAll()) {
+ if (host->GetURL() == extension_url) {
+ subframe_agent_host = host;
+ break;
+ }
+ }
+ ASSERT_TRUE(subframe_agent_host);
+ EXPECT_TRUE(IsInspectionAllowed(profile(), subframe_agent_host.get()));
+}
#endif // !BUILDFLAG(IS_ANDROID)
class DevToolsAllowedByCommandLineSwitch
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page