CVE-2026-95300
Overview
Background
- DevTools frontend
- the HTML/JS user interface that drives Chrome’s inspector, which communicates with the browser process through
DevToolsUIBindings. - `is_local_frontend_`
- a boolean member of
DevToolsUIBindingsthat is true only when the frontend page is served from a trusted local origin (e.g. bundleddevtools://resources) rather than a remote host. - Remote DevTools frontend
- a DevTools UI loaded from a remote/untrusted URL (for example via remote debugging or a custom frontend URL), which can invoke the same binding methods as a local frontend.
- Privileged bindings
DevToolsUIBindingsmethods that reach out to the local machine (file system access, indexing/searching, port-forwarding config, device updates, browser restart) and thus require elevated trust.
Root Cause Analysis
The privileged methods on DevToolsUIBindings — such as Save, AppendToFile, RequestFileSystems, AddFileSystem, IndexPath, SearchInPath, and SetDevicesUpdatesEnabled/port-forwarding config — validated only that the caller came through a valid frontend URL (IsValidFrontendURL) and had a live frontend_host_, but never checked whether that frontend was actually local. Because a remote DevTools frontend passes the IsValidFrontendURL gate just like a local one, the invariant that “only trusted local frontends may perform local file-system and device operations” was violated, letting a remote frontend drive the browser process into reading, writing, and enumerating the user’s files and reconfiguring device/port-forwarding state.
The fix adds an early if (!is_local_frontend_) guard to each of these methods so a non-local frontend returns immediately (or calls the appropriate completion callback with an empty/failure result) instead of executing the privileged operation. This works because is_local_frontend_ is established from the trusted origin of the frontend at binding time, so the check cannot be satisfied by a remote page, closing the authorization gap while preserving graceful client-side behavior.
is_local_frontend_ before any privileged work runs.Attack Path
- Load a remote DevTools frontend
An attacker gets the victim to open, or the victim configures, a DevTools frontend served from a remote/untrusted URL that still satisfies
IsValidFrontendURL. - Invoke privileged bindings
From that remote frontend, the attacker calls binding methods like
Save,AppendToFile,AddFileSystem,RequestFileSystems, orIndexPath, which previously ran without checkingis_local_frontend_. - Reach local operations
The unguarded methods forward directly to
file_helper_and device/port-forwarding logic in the browser process, performing local file writes, file-system enumeration, indexing/search, or device configuration on the victim’s machine. - Escalate to file exfiltration or config tampering (inferred) By combining file-system enumeration with save/append and port-forwarding changes, a remote frontend could read out or modify local files and alter device connectivity, though the diff itself only shows the removed authorization boundary, not a specific end-to-end exploit.
Impact Assessment
IsValidFrontendURL. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.8. Any escalation to full local file exfiltration or persistent device reconfiguration beyond the removed checks is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/devtools/devtools_ui_bindings.cc |
modified | |
DevToolsUIBindingsLoadNetworkResourceTestchrome/browser/devtools/devtools_ui_bindings_unittest.cc |
modified |
Files Changed
chrome/browser/devtools/devtools_ui_bindings_unittest.ccchrome/browser/devtools/devtools_ui_bindings.cc
Audit Directions
- URL-validity vs. trust conflationAudit other binding/IPC entry points that gate on
IsValidFrontendURLorfrontend_host_alone and confirm they also checkis_local_frontend_(or an equivalent trust signal) before privileged work. - Privileged sinks in `DevToolsUIBindings`Enumerate every method that reaches
file_helper_, device/port-forwarding config, or process lifecycle (e.g. browser restart, device updates) and verify each has an authorization check, since new methods can be added without one. - Graceful-failure completenessReview methods with client callbacks (
*Done,*Completed,*Loaded,Canceled*) to ensure early-return guards still invoke the expected callback so the fix cannot be bypassed by hanging the frontend into an alternate code path.
Patch
From a39fcdb1b98f19c76e0f0eff9d74f8d631816813 Mon Sep 17 00:00:00 2001 From: Danil Somsikov <[email protected]> Date: Tue, 25 Aug 2026 01:01:51 -0700 Subject: [PATCH] Restrict privileged DevTools APIs to local frontends. This change prevents remote DevTools frontends from accessing privileged APIs, such as file system operations, port forwarding configuration, device updates, and browser restart. These methods now check is_local_frontend_ and return early or fail gracefully if the frontend is not local. A unit test has been added to verify this restriction. Bug: 545449081 Change-Id: Ibe07b39661a8827c850a73a67e483cc8a011f252 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8285238 Auto-Submit: Danil Somsikov <[email protected]> Commit-Queue: Danil Somsikov <[email protected]> Reviewed-by: Alex Rudenko <[email protected]> Cr-Commit-Position: refs/heads/main@{#1685335} --- diff --git a/chrome/browser/devtools/devtools_ui_bindings.cc b/chrome/browser/devtools/devtools_ui_bindings.cc index fd6e29b..5771aa0 100644 --- a/chrome/browser/devtools/devtools_ui_bindings.cc +++ b/chrome/browser/devtools/devtools_ui_bindings.cc @@ -1423,6 +1423,9 @@ void DevToolsUIBindings::ShowItemInFolder(const std::string& file_system_path) { CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + return; + } if (!file_helper_.IsFileInFileSystem(file_system_path)) { return; } @@ -1433,6 +1436,10 @@ const std::string& content, bool save_as, bool is_base64) { + if (!is_local_frontend_) { + CanceledFileSaveAs(url); + return; + } file_helper_.Save( url, content, save_as, is_base64, base::BindOnce(&DevToolsSelectFileDialog::SelectFile, web_contents_, @@ -1445,6 +1452,9 @@ void DevToolsUIBindings::AppendToFile(const std::string& url, const std::string& content) { + if (!is_local_frontend_) { + return; + } file_helper_.Append(url, content, base::BindOnce(&DevToolsUIBindings::AppendedTo, weak_factory_.GetWeakPtr(), url)); @@ -1453,6 +1463,12 @@ void DevToolsUIBindings::RequestFileSystems() { CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + base::ListValue empty_file_systems_value; + CallClientMethod("DevToolsAPI", "fileSystemsLoaded", + base::Value(std::move(empty_file_systems_value))); + return; + } base::ListValue file_systems_value; for (auto const& file_system : file_helper_.GetFileSystems()) { file_systems_value.Append(CreateFileSystemValue(file_system)); @@ -1464,6 +1480,10 @@ void DevToolsUIBindings::AddFileSystem(const std::string& type) { CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + FileSystemAdded("Restricted to local DevTools", nullptr); + return; + } file_helper_.AddFileSystem( type, base::BindOnce(&DevToolsSelectFileDialog::SelectFile, web_contents_, @@ -1475,6 +1495,9 @@ void DevToolsUIBindings::RemoveFileSystem(const std::string& file_system_path) { CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + return; + } file_helper_.RemoveFileSystem(file_system_path); } @@ -1482,6 +1505,9 @@ const std::string& file_system_url) { CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + return; + } file_helper_.UpgradeDraggedFileSystemPermissions( file_system_url, base::BindRepeating(&DevToolsUIBindings::HandleDirectoryPermissions, @@ -1496,6 +1522,10 @@ DCHECK_CURRENTLY_ON(BrowserThread::UI); CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + ConnectAutomaticFileSystemDone(std::move(callback), false); + return; + } // Ensure that the |file_system_uuid| is indeed a valid UUID. base::Uuid uuid = base::Uuid::ParseCaseInsensitive(file_system_uuid); if (!uuid.is_valid()) { @@ -1527,6 +1557,9 @@ DCHECK_CURRENTLY_ON(BrowserThread::UI); CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + return; + } file_helper_.DisconnectAutomaticFileSystem(file_system_path); } @@ -1537,6 +1570,10 @@ DCHECK_CURRENTLY_ON(BrowserThread::UI); CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + IndexingDone(index_request_id, file_system_path); + return; + } if (!file_helper_.IsFileSystemAdded(file_system_path)) { IndexingDone(index_request_id, file_system_path); return; @@ -1586,6 +1623,11 @@ DCHECK_CURRENTLY_ON(BrowserThread::UI); CHECK(IsValidFrontendURL(web_contents_->GetLastCommittedURL()) && frontend_host_); + if (!is_local_frontend_) { + SearchCompleted(search_request_id, file_system_path, + std::vector<std::string>()); + return; + } if (!file_helper_.IsFileSystemAdded(file_system_path)) { SearchCompleted(search_request_id, file_system_path, std::vector<std::string>()); @@ -1627,6 +1669,9 @@ const std::string& port_forwarding_config, bool network_discovery_enabled, const std::string& network_discovery_config) { + if (!is_local_frontend_) { + return; + } std::optional<base::DictValue> parsed_port_forwarding = base::JSONReader::ReadDict(port_forwarding_config, base::JSON_PARSE_CHROMIUM_EXTENSIONS); @@ -1687,6 +1732,9 @@ } void DevToolsUIBindings::SetDevicesUpdatesEnabled(bool enabled) { + if (!is_local_frontend_ && enabled) { + return; + } #if BUILDFLAG(IS_ANDROID) NOTIMPLEMENTED(); #else @@ -1736,6 +1784,9 @@ void DevToolsUIBindings::OpenRemotePage(const std::string& browser_id, const std::string& url) { + if (!is_local_frontend_) { + return; + } if (!remote_targets_handler_) { return; } @@ -1743,6 +1794,9 @@ } void DevToolsUIBindings::OpenNodeFrontend() { + if (!is_local_frontend_) { + return; + } delegate_->OpenNodeFrontend(); } @@ -2459,10 +2513,16 @@ void DevToolsUIBindings::SetChromeFlag(const std::string& flag_name, bool value) { + if (!is_local_frontend_) { + return; + } SetChromeFlagInternal(profile_, flag_name, value); } void DevToolsUIBindings::RequestRestart() { + if (!is_local_frontend_) { + return; + } chrome::AttemptRestart(); } @@ -3163,6 +3223,9 @@ frontend_loaded_ = false; is_local_frontend_ = IsLocalDevToolsFrontendURL(web_contents_->GetLastCommittedURL()); + if (!is_local_frontend_) { + SetDevicesUpdatesEnabled(false); + } } void DevToolsUIBindings::FrontendLoaded() { diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc index fd10452..7885666a 100644 --- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc +++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc @@ -42,6 +42,9 @@ class DevToolsUIBindingsLoadNetworkResourceTest : public testing::Test { public: + bool GetDevicesUpdatesEnabled() { + return bindings_->devices_updates_enabled_; + } void SetUp() override { profile_ = std::make_unique<TestingProfile>(); web_contents_ = web_contents_factory_.CreateWebContents(profile_.get()); @@ -106,6 +109,19 @@ }; TEST_F(DevToolsUIBindingsLoadNetworkResourceTest, + RestrictsPrivilegedMethodsFromRemoteFrontend) { + content::NavigationSimulator::NavigateAndCommitFromBrowser( + web_contents(), + GURL("devtools://devtools/remote/serve_file/inspector.html")); + + // Should return early without enabling device updates. + static_cast<DevToolsEmbedderMessageDispatcher::Delegate*>(bindings()) + ->SetDevicesUpdatesEnabled(true); + + EXPECT_FALSE(GetDevicesUpdatesEnabled()); +} + +TEST_F(DevToolsUIBindingsLoadNetworkResourceTest, AllowsFileSchemeFromRemoteFrontendWithFlag) { base::test::ScopedCommandLine scoped_command_line; scoped_command_line.GetProcessCommandLine()->AppendSwitch(
Regression Test / PoC
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index fd10452..7885666a 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -42,6 +42,9 @@
class DevToolsUIBindingsLoadNetworkResourceTest : public testing::Test {
public:
+ bool GetDevicesUpdatesEnabled() {
+ return bindings_->devices_updates_enabled_;
+ }
void SetUp() override {
profile_ = std::make_unique<TestingProfile>();
web_contents_ = web_contents_factory_.CreateWebContents(profile_.get());
@@ -106,6 +109,19 @@
};
TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+ RestrictsPrivilegedMethodsFromRemoteFrontend) {
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ web_contents(),
+ GURL("devtools://devtools/remote/serve_file/inspector.html"));
+
+ // Should return early without enabling device updates.
+ static_cast<DevToolsEmbedderMessageDispatcher::Delegate*>(bindings())
+ ->SetDevicesUpdatesEnabled(true);
+
+ EXPECT_FALSE(GetDevicesUpdatesEnabled());
+}
+
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
AllowsFileSchemeFromRemoteFrontendWithFlag) {
base::test::ScopedCommandLine scoped_command_line;
scoped_command_line.GetProcessCommandLine()->AppendSwitch(
Original Bug Report
Inadequate privilege separation for Remote DevTools Frontends bypasses browser security
Vulnerability Details:
Chrome DevTools does not adequately isolate the privileges of remote frontends (served from chrome-devtools-frontend.appspot.com via the devtools://devtools/remote/ path). Because Chrome does not origin-isolate different versions of DevTools, a compromised remote frontend shares window.localStorage with the local frontend.
More critically, remote frontends have access to C++ embedder bindings that lack is_local_frontend_ authorization checks, leading to severe privilege escalation:
DevToolsUIBindings::SetChromeFlagandRequestRestart: A remote frontend can callHost.InspectorFrontendHost.InspectorFrontendHostInstance.setChromeFlag()to write directly to the browser’s global preferences viaabout_flags::SetFeatureEntryEnabled. It can then callrequestRestart(), which immediately triggerschrome::AttemptRestart().DevToolsUIBindings::OpenInNewTab: Lacks local frontend checks, allowing a remote frontend to silently open arbitraryhttp://andhttps://background tabs for DoS or phishing.DevToolsUIBindings::SetPreference: Writes directly to the globalprefs::kDevToolsPreferencesdictionary tied to the user’s Chrome Profile without checking if the frontend is local, causing global preference pollution.
Impact:
An attacker who exploits a known XSS in a historical, legacy DevTools frontend can permanently compromise the developer’s browser. By executing setChromeFlag('disable-web-security', true) and requestRestart(), the attacker forces the entire Chrome browser to restart immediately with security features permanently disabled. They can also pollute global DevTools settings and execute phishing attacks via tab hijacking.
Reproduction Steps (Attack Scenario):
- An attacker sets up a malicious target (e.g., a malicious Android app or web page exposing a remote debugging port) that explicitly advertises an old, vulnerable V8 version.
- The developer connects to this target via
chrome://inspect. - Chrome fetches and loads the corresponding legacy remote frontend from
chrome-devtools-frontend.appspot.comwhich contains a known XSS flaw. - The attacker exploits the XSS to run JavaScript in the
devtools://devtoolsorigin. - The attacker script calls
setChromeFlag('disable-web-security', true)andrequestRestart(). - The developer’s browser restarts with web security disabled.
Suggested Fix:
- C++ Hardening: Add explicit
if (!is_local_frontend_) return;early-exit checks toDevToolsUIBindings::SetChromeFlag,DevToolsUIBindings::RequestRestart,DevToolsUIBindings::OpenInNewTab, andDevToolsUIBindings::OpenRemotePage. - C++ Preference Isolation: Modify
DevToolsUIBindings::SetPreferenceso that if!is_local_frontend_, it writes to an ephemeral store or a separateprefs::kDevToolsRemotePreferencesdictionary so it cannot overwrite local session state. - Architectural Origin Isolation (Long-term): Serve remote frontends from a dedicated scheme like
devtools-remote://devtools/...instead of sharing thedevtools://scheme. This will enforce the Same-Origin Policy, natively sandboxingwindow.localStorageand preventing storage collisions like thewatch-expressionsleak.