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 <[email protected]> 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 <[email protected]> Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Kateryna Prokopenko <[email protected]> Reviewed-by: Nikolay Vitkov <[email protected]> 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