Overview

Medium
Severity
4.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in DevTools
DescriptionMissing authorization in DevTools
ComponentDevTools
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker545449081
Fix commita39fcdb1b98f (chromium/src) +79/-0
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-08-12 (per vendor advisory)
Fix landed2026-08-25 (public git)
Disclosed2026-09-22
Patch gap28 days — fix public in source before the advisory

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 DevToolsUIBindings that is true only when the frontend page is served from a trusted local origin (e.g. bundled devtools:// 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
DevToolsUIBindings methods 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.

Key insight
The core mistake was conflating “valid frontend URL” with “trusted local frontend,” so privileged local-machine APIs were exposed to remote frontends; the fix restores the missing authorization by gating every such method on is_local_frontend_ before any privileged work runs.

Attack Path

  1. 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.
  2. Invoke privileged bindings From that remote frontend, the attacker calls binding methods like Save, AppendToFile, AddFileSystem, RequestFileSystems, or IndexPath, which previously ran without checking is_local_frontend_.
  3. 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.
  4. 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

An attacker controlling a remote DevTools frontend gains the ability to invoke privileged local operations — file save/append, file-system enumeration and indexing, port-forwarding/device configuration, and related actions — executed in Chrome’s trusted browser process on the victim’s machine. The preconditions are that a remote (non-local) DevTools frontend is loaded and permitted to issue binding calls that pass 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

FunctionChangeNotes
if
chrome/browser/devtools/devtools_ui_bindings.cc
modified
DevToolsUIBindingsLoadNetworkResourceTest
chrome/browser/devtools/devtools_ui_bindings_unittest.cc
modified

Files Changed

  • chrome/browser/devtools/devtools_ui_bindings_unittest.cc
  • chrome/browser/devtools/devtools_ui_bindings.cc

Audit Directions

  • URL-validity vs. trust conflation
    Audit other binding/IPC entry points that gate on IsValidFrontendURL or frontend_host_ alone and confirm they also check is_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 completeness
    Review 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.
From a39fcdb1b98f19c76e0f0eff9d74f8d631816813 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
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 <dsv@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
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(
Loading diff…

Regression Test / PoC

shipped with the fix
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(
Loading diff…

Original Bug Report

reported by da...@google.com

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:

  1. DevToolsUIBindings::SetChromeFlag and RequestRestart: A remote frontend can call Host.InspectorFrontendHost.InspectorFrontendHostInstance.setChromeFlag() to write directly to the browser’s global preferences via about_flags::SetFeatureEntryEnabled. It can then call requestRestart(), which immediately triggers chrome::AttemptRestart().
  2. DevToolsUIBindings::OpenInNewTab: Lacks local frontend checks, allowing a remote frontend to silently open arbitrary http:// and https:// background tabs for DoS or phishing.
  3. DevToolsUIBindings::SetPreference: Writes directly to the global prefs::kDevToolsPreferences dictionary 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):

  1. 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.
  2. The developer connects to this target via chrome://inspect.
  3. Chrome fetches and loads the corresponding legacy remote frontend from chrome-devtools-frontend.appspot.com which contains a known XSS flaw.
  4. The attacker exploits the XSS to run JavaScript in the devtools://devtools origin.
  5. The attacker script calls setChromeFlag('disable-web-security', true) and requestRestart().
  6. The developer’s browser restarts with web security disabled.

Suggested Fix:

  1. C++ Hardening: Add explicit if (!is_local_frontend_) return; early-exit checks to DevToolsUIBindings::SetChromeFlag, DevToolsUIBindings::RequestRestart, DevToolsUIBindings::OpenInNewTab, and DevToolsUIBindings::OpenRemotePage.
  2. C++ Preference Isolation: Modify DevToolsUIBindings::SetPreference so that if !is_local_frontend_, it writes to an ephemeral store or a separate prefs::kDevToolsRemotePreferences dictionary so it cannot overwrite local session state.
  3. Architectural Origin Isolation (Long-term): Serve remote frontends from a dedicated scheme like devtools-remote://devtools/... instead of sharing the devtools:// scheme. This will enforce the Same-Origin Policy, natively sandboxing window.localStorage and preventing storage collisions like the watch-expressions leak.
View on issue tracker