CVE-2025-13632
Overview
Background
- `devtools://` scheme
- the internal scheme (
content::kChromeDevToolsScheme) that serves the privileged DevTools front-end, which talks to the browser through DevTools bindings rather than ordinary web APIs. - `ExtensionTabUtil`
- browser-side helper in
chrome/browser/extensions/extension_tab_util.ccthat validates and normalises URLs that extension APIs (for examplechrome.tabs.create/chrome.windows.create) ask the browser to open. - `devtools` / `debugger` permissions
- extension capabilities (
APIPermissionID::kDevtools, granted by adevtools_pagemanifest key, andAPIPermissionID::kDebugger) that let an extension extend DevTools or attach to targets via the debugger protocol. - `kCannotNavigateToDevtools`
- the error string returned to the extension when a navigation to a
devtools://URL is refused.
Root Cause Analysis
The URL-preparation routine in extension_tab_util.cc refused extension-initiated navigations to devtools:// URLs, but contained an explicit carve-out: if the calling extension held either the kDevtools or kDebugger API permission, has_permission was true and the URL was allowed through. This let an extension, rather than the user, open the DevTools front-end in a tab or window, which is a privileged browser surface; the enclosing function name is not shown in the hunk, but the shipped unit test exercises this exact branch through ExtensionTabUtil::PrepareURLForNavigation. The invariant the patch enforces is that opening DevTools must be user-initiated, so holding an extension permission should not be enough.
The fix removes the permission check entirely, so any URL whose scheme is kChromeDevToolsScheme now returns base::unexpected(kCannotNavigateToDevtools) unconditionally. This makes the check stricter, and the error text in extension_tab_util.h is changed to match (“Cannot navigate to a devtools:// page.”). The unit tests that asserted devtools/debugger extensions could navigate to the DevTools URL are deleted.
devtools/debugger permission” as equivalent to “may programmatically open the DevTools front-end”, giving extensions a way into a privileged page that should only open when the user starts it. The fix deletes the exception so devtools:// navigations from extension APIs are always rejected.Attack Path
- Install extension with qualifying permission
The attacker gets the user to install an extension that declares a
devtools_pageor thedebuggerpermission, which pre-patch satisfied thehas_permissioncarve-out. - Request a `devtools://` URL via extension API
The extension asks the browser to create or navigate a tab/window to a
devtools://URL, which passes through the URL-preparation check inextension_tab_util.cc. - Check passes on permission
Because
HasAPIPermission(kDevtools)orHasAPIPermission(kDebugger)is true, the pre-patch code returns the URL instead ofkCannotNavigateToDevtools, and the DevTools front-end opens without the user starting it. - Abuse privileged front-end (inferred) The extension could then try to drive or interact with the DevTools front-end in ways that go beyond what its own extension APIs allow (inferred; the patch does not show any specific follow-on primitive).
Impact Assessment
devtools or debugger permission could open the DevTools front-end (devtools://) in a tab or window on its own. The required precondition is an installed extension with one of those permissions; ordinary web content cannot reach this check, and no compromised renderer is needed. The vendor severity is High; any escalation through the DevTools front-end’s privileged bindings beyond simply opening the page is not shown by the patch and is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/extension_tab_util.cc |
modified |
Files Changed
chrome/browser/extensions/extension_tab_util.ccchrome/browser/extensions/extension_tab_util.hchrome/browser/extensions/extension_tab_util_unittest.cc
Audit Directions
- Permission-gated carve-outs to privileged schemesReview other scheme blocks in the same routine (for example
chrome-untrusted://andchrome://) and in other extension navigation paths for “allowed if extension has permission X” exceptions that let an API permission stand in for user intent. - Alternative entry points to the same surfaceCheck whether
devtools://can still be reached through other extension-controlled paths (for examplechrome.windows.createURL lists,chrome.tabs.update, redirects, orwebNavigation-driven navigations) that may skip this URL-preparation helper. - Manifest-key-derived permissionsLook at implicit permissions granted by manifest keys (such as
devtools_page) to see whether they unlock browser-privileged behaviour beyond their intended scope.
Patch
From 2f952b7796483626f16d5995221b4dca8c3680c2 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Mon, 13 Oct 2025 14:12:00 -0700
Subject: [PATCH] [Extensions] Block devtools window / tab creation from extension APIs
We currently have a carve-out that allows extensions with the devtools
or debugger permissions to create a devtools window. Even with these
permissions, this is something we'd prefer be left up to the user to
initiate. Don't allow these navigations.
Bug: 439058242
Change-Id: I402d52ffbbd6f8537df92d12a3211ca70ff7ae3c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7030016
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Reviewed-by: Oliver Dunk <oliverdunk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1529133}
---
diff --git a/chrome/browser/extensions/extension_tab_util.cc b/chrome/browser/extensions/extension_tab_util.cc
index 6d3c3a95..1173528 100644
--- a/chrome/browser/extensions/extension_tab_util.cc
+++ b/chrome/browser/extensions/extension_tab_util.cc
@@ -1341,17 +1341,9 @@
return base::unexpected(kNoCrashBrowserError);
}
- // Don't let the extension navigate directly to devtools scheme pages, unless
- // they have applicable permissions.
+ // Don't let the extension navigate directly to devtools scheme pages.
if (url.SchemeIs(content::kChromeDevToolsScheme)) {
- bool has_permission =
- extension && (extension->permissions_data()->HasAPIPermission(
- APIPermissionID::kDevtools) ||
- extension->permissions_data()->HasAPIPermission(
- APIPermissionID::kDebugger));
- if (!has_permission) {
- return base::unexpected(kCannotNavigateToDevtools);
- }
+ return base::unexpected(kCannotNavigateToDevtools);
}
// Don't let the extension navigate directly to chrome-untrusted scheme pages.
diff --git a/chrome/browser/extensions/extension_tab_util.h b/chrome/browser/extensions/extension_tab_util.h
index aa2d679f..fb5e272 100644
--- a/chrome/browser/extensions/extension_tab_util.h
+++ b/chrome/browser/extensions/extension_tab_util.h
@@ -82,8 +82,7 @@
"Browser windows not allowed.";
#endif // !BUILDFLAG(IS_ANDROID)
static constexpr char kCannotNavigateToDevtools[] =
- "Cannot navigate to a devtools:// page without either the devtools or "
- "debugger permission.";
+ "Cannot navigate to a devtools:// page.";
#if !BUILDFLAG(IS_ANDROID)
static constexpr char kLockedFullscreenModeNewTabError[] =
"You cannot create new tabs while in locked fullscreen mode.";
diff --git a/chrome/browser/extensions/extension_tab_util_unittest.cc b/chrome/browser/extensions/extension_tab_util_unittest.cc
index 603c0655..5b290ed4 100644
--- a/chrome/browser/extensions/extension_tab_util_unittest.cc
+++ b/chrome/browser/extensions/extension_tab_util_unittest.cc
@@ -249,23 +249,6 @@
EXPECT_THAT(
url, base::test::ErrorIs(ExtensionTabUtil::kCannotNavigateToDevtools));
}
- // Having the devtools permissions should allow access.
- {
- auto devtools_extension = ExtensionBuilder("devtools")
- .SetManifestKey("devtools_page", "foo.html")
- .Build();
- auto url = ExtensionTabUtil::PrepareURLForNavigation(
- kDevtoolsURL, devtools_extension.get(), browser_context());
- EXPECT_THAT(url, base::test::ValueIs(kDevtoolsURL));
- }
- // Having the debugger permissions should also allow access.
- {
- auto debugger_extension =
- ExtensionBuilder("debugger").AddAPIPermission("debugger").Build();
- auto url = ExtensionTabUtil::PrepareURLForNavigation(
- kDevtoolsURL, debugger_extension.get(), browser_context());
- EXPECT_THAT(url, base::test::ValueIs(kDevtoolsURL));
- }
}
TEST_F(ChromeExtensionNavigationTest,
Regression Test / PoC
diff --git a/chrome/browser/extensions/extension_tab_util_unittest.cc b/chrome/browser/extensions/extension_tab_util_unittest.cc
index 603c0655..5b290ed4 100644
--- a/chrome/browser/extensions/extension_tab_util_unittest.cc
+++ b/chrome/browser/extensions/extension_tab_util_unittest.cc
@@ -249,23 +249,6 @@
EXPECT_THAT(
url, base::test::ErrorIs(ExtensionTabUtil::kCannotNavigateToDevtools));
}
- // Having the devtools permissions should allow access.
- {
- auto devtools_extension = ExtensionBuilder("devtools")
- .SetManifestKey("devtools_page", "foo.html")
- .Build();
- auto url = ExtensionTabUtil::PrepareURLForNavigation(
- kDevtoolsURL, devtools_extension.get(), browser_context());
- EXPECT_THAT(url, base::test::ValueIs(kDevtoolsURL));
- }
- // Having the debugger permissions should also allow access.
- {
- auto debugger_extension =
- ExtensionBuilder("debugger").AddAPIPermission("debugger").Build();
- auto url = ExtensionTabUtil::PrepareURLForNavigation(
- kDevtoolsURL, debugger_extension.get(), browser_context());
- EXPECT_THAT(url, base::test::ValueIs(kDevtoolsURL));
- }
}
TEST_F(ChromeExtensionNavigationTest,
Original Bug Report
Extensions can run JS on any privileged origin by exploiting already-patched vulnerabilities under devtools:// scheme.
VULNERABILITY DETAILS
The devtools:// scheme allows loading of remote assets from the domain https://chrome-devtools-frontend.appspot.com/. This server keeps a history of old versions of the DevTools Frontend, which can be loaded by navigating any tab to the URL “devtools://devtools/remote/serve_file/@<commit_hash>/<filename>”.
When loaded this way, these assets are fully-privileged, with access to the DevToolsAPI object. Because these are old versions, they contain many vulnerabilities that have already been patched, effectively allowing the exploitation of such vulnerabilities even in the latest Chrome version.
EXPLANATION
An extension can open a new tab and browse to a devtools:// URL that loads an old version of the DevTools frontend from the remote server, then exploit any old vulnerability to achieve code execution under the DevTools page. This allows a malicious extension to obtain access to the DevToolsAPI object, which can do many privileged operations, such as accessing local files, impersonating other extensions (even privileged ones), and performing universal XSS. All of this can be done with no user interaction, other than just installing the malicious extension.
In addition to that, the extension can also setup “Console Pins”, by writing to localStorage when running code under the old DevTools Frontend. These Console Pins will then execute in ANY privileged origin, allowing a full sandbox escape, as soon as the user opens the real DevTools console. It’s not needed to open the console in a privileged page, just by opening console anywhere, the extension can navigate the inspected tab to a privileged page, making the Console Pin execute there.
Another way to escape sandbox, is by using DevTools to impersonate a highly-privileged extension such as Perfetto UI, which can control the entire browser (its id is hardcoded in chromium sources). This is demonstrated in the attached “PoC 2”. If Perfetto UI is already installed, no user interaction is required, if not, we use the webstorePrivate API to show an extension install prompt (we can even change name and icon, to make it look like our own extension). If the user accepts the prompt, Perfetto UI will be installed, and we will impersonate it.
VERSION
Chrome Version: 139.0.7258.128 stable
Operating System: Tested on Windows 11, should work on all operating systems.
REPRODUCTION CASE
In this PoC, we are reusing the vulnerability described in https://issues.chromium.org/issues/40051844. This vulnerability has been patched long ago, but because we are loading an old DevTools frontend, we can reuse it.
Steps to reproduce:
- Download the attached “PoC 1.zip” file and install it as an unpacked extension
- As soon as the extension is installed, it will open a new window with the url devtools://devtools/remote/serve_file/@e22de67c28798d98833a7137c0e22876237fc40a/inspector.html.
- After a few seconds, the extension will write the contents of the C:\ directory to the body of the DevTools page, demonstrating that it achieved local file access through the DevToolsAPI object. No user interaction is required up to this step.
- The extension will also setup a Console Pin, and wait for DevTools to be opened. As soon as you press F12 in any page, that page will be navigated to chrome://inspect, and the Console Pin will execute there.
- The script injected in chrome://inspect will open the Node Frontend, then it will open a DevTools console to inspect the Node Frontend. This will cause the Console Pin to be injected in the Node Frontend too.
- Since the Node Frontend is attached to the browser target, the injected script has full control over the browser. The script will download calc.exe and use the chrome://downloads page to execute it.
- At the end of the process, the extension will remove the console pin and uninstall itself, to avoid an infinite loop, and to not interefere with PoC #2.
The second PoC demonstrates the usage of the webstorePrivate API, to spoof a WebStore prompt, tricking the user into installing the Perfetto UI extension, despite the name and icon in the prompt being completely different. This uses the same vulnerability as above, but it is much stealthier: No UI is displayed to the user, other than a minimized DevTools window that exists briefly when the extension is loaded. It also performs a full sandbox escape, downloading and executing calc.exe in the end.
Steps to reproduce:
- Download and install “PoC 2.zip” as an unpacked extension.
- A minimized, barely perceptible, DevTools window, will appear in the taskbar for some seconds, and then close itself.
- After that, open a new tab and paste an URL of the Chrome Web Store, for ANY extension (ex. https://chromewebstore.google.com/detail/framesexplorer/imijdbpfemdegalijeojlkhiamfcgklp). After the page is fully loaded, click on the install button, and accept the prompt.
- As soon as the installation of your chosen extension is finished, you will see an alert in the WebStore page saying “Failed to install <extension>, please try again”. The alert shows “Web Store extension” as the origin.
- After accepting the alert, you will see a second install prompt, that looks exactly like the prompt you just accepted (same name and icon). This would lead a user to believe that the installation “failed”, and the browser is “trying again”. But in reality, this second prompt, if accepted, will install the Perfetto UI extension.
- As soon as Perfetto UI is installed, the extension will open another minimized DevTools window, to impersonate the Perfetto UI extension, and take control of the browser. After a few seconds, calc.exe will be downloaded and launched.
CREDIT INFORMATION
Reporter credit: Leandro Teles (https://x.com/leandrotp2, https://www.linkedin.com/in/leandro-teles-212014197/)