CVE-2026-13929
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/devtools/protocol/target_handler_android.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/extensions/api/debugger/debugger_apitest.cc |
modified |
Files Changed
chrome/browser/devtools/protocol/target_handler_android.ccchrome/browser/devtools/protocol/target_handler_android.hchrome/browser/extensions/api/debugger/debugger_apitest.cccontent/browser/devtools/render_frame_devtools_agent_host.cc
Patch
From 8bd52544c5cfb9b1108cc9235dc24885e2525aa5 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Tue, 19 May 2026 09:54:00 -0700
Subject: [PATCH] [DevTools] Add missing trust checks in TargetHandlerAndroid::CreateTarget
Untrusted DevTools clients were able to bypass restrictions and open
arbitrary `devtools://`, `chrome-untrusted://` URLs by using the
Target.createTarget CDP command on Android. The desktop implementation
(TargetHandler::CreateTarget) was previously patched for this issue
(crbug.com/503197481), but the Android port accepted the `is_trusted` and
`may_read_local_files` parameters in its constructor without ever
storing or evaluating them.
This CL fixes the vulnerability by:
1. Storing `is_trusted` and `may_read_local_files` as member variables
in `TargetHandlerAndroid`.
2. Adding URL scheme validation in `TargetHandlerAndroid::CreateTarget`
to reject privileged schemes from untrusted clients and block
creation of targets with local files when not permitted, matching
the desktop behavior.
3. Enabling the `CreateTargetToUntrustedWebUI` test for Android in
`debugger_apitest.cc` to ensure ongoing coverage.
Bug: 512249559
Change-Id: I304e44a5585288f81197252b090bb4d6dab0b1a1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7857154
Reviewed-by: Finnur Thorarinsson <finnur@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632957}
---
diff --git a/chrome/browser/devtools/protocol/target_handler_android.cc b/chrome/browser/devtools/protocol/target_handler_android.cc
index 578fe99e..70ffafbc 100644
--- a/chrome/browser/devtools/protocol/target_handler_android.cc
+++ b/chrome/browser/devtools/protocol/target_handler_android.cc
@@ -8,12 +8,16 @@
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "content/public/browser/web_contents.h"
+#include "content/public/common/url_constants.h"
+#include "content/public/common/url_utils.h"
+#include "url/url_constants.h"
using content::WebContents;
TargetHandlerAndroid::TargetHandlerAndroid(protocol::UberDispatcher* dispatcher,
bool is_trusted,
- bool may_read_local_files) {
+ bool may_read_local_files)
+ : is_trusted_(is_trusted), may_read_local_files_(may_read_local_files) {
protocol::Target::Dispatcher::wire(dispatcher, this);
}
@@ -58,8 +62,29 @@
TabModel* tab_model = models[0];
CHECK(tab_model);
+ GURL gurl(url);
+ if (gurl.is_empty()) {
+ gurl = GURL(url::kAboutBlankURL);
+ }
+
+ GURL inner_url = gurl;
+ if (gurl.SchemeIs(content::kViewSourceScheme)) {
+ inner_url = GURL(gurl.GetContent());
+ }
+
+ if (!is_trusted_ && (inner_url.SchemeIs(content::kChromeUIUntrustedScheme) ||
+ inner_url.SchemeIs(content::kChromeDevToolsScheme))) {
+ return protocol::Response::ServerError(
+ "Navigating to a URL with a privileged scheme is not allowed");
+ }
+
+ if (!may_read_local_files_ && inner_url.SchemeIsFile()) {
+ return protocol::Response::ServerError(
+ "Creating a target with a local URL is not allowed");
+ }
+
WebContents* web_contents =
- tab_model->CreateNewTabForDevTools(GURL(url), new_window.value_or(false));
+ tab_model->CreateNewTabForDevTools(gurl, new_window.value_or(false));
if (!web_contents) {
return protocol::Response::ServerError("Could not create a Tab");
}
diff --git a/chrome/browser/devtools/protocol/target_handler_android.h b/chrome/browser/devtools/protocol/target_handler_android.h
index ba693fab6..c092c1d 100644
--- a/chrome/browser/devtools/protocol/target_handler_android.h
+++ b/chrome/browser/devtools/protocol/target_handler_android.h
@@ -46,6 +46,8 @@
std::string* out_target_id) override;
private:
+ bool is_trusted_ = false;
+ bool may_read_local_files_ = false;
RemoteLocations remote_locations_;
};
diff --git a/chrome/browser/extensions/api/debugger/debugger_apitest.cc b/chrome/browser/extensions/api/debugger/debugger_apitest.cc
index 4570311..934b79e 100644
--- a/chrome/browser/extensions/api/debugger/debugger_apitest.cc
+++ b/chrome/browser/extensions/api/debugger/debugger_apitest.cc
@@ -1150,7 +1150,6 @@
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
-// TODO(crbug.com/441339825): Fails on desktop Android.
// Tests that Target.createTarget to WebUI origins are blocked.
IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, CreateTargetToUntrustedWebUI) {
ASSERT_TRUE(RunExtensionTest("debugger_create_target_to_untrusted_webui"))
diff --git a/content/browser/devtools/render_frame_devtools_agent_host.cc b/content/browser/devtools/render_frame_devtools_agent_host.cc
index b55c73d0..c6cd2b7 100644
--- a/content/browser/devtools/render_frame_devtools_agent_host.cc
+++ b/content/browser/devtools/render_frame_devtools_agent_host.cc
@@ -579,8 +579,9 @@
RenderFrameHostImpl* old_host = frame_host_;
ChangeFrameHostAndObservedProcess(frame_host);
- if (IsAttached())
+ if (IsAttached()) {
UpdateRawHeadersAccess(old_host, nullptr);
+ }
UpdateFrameAlive();
}
Regression Test / PoC
diff --git a/chrome/browser/extensions/api/debugger/debugger_apitest.cc b/chrome/browser/extensions/api/debugger/debugger_apitest.cc
index 4570311..934b79e 100644
--- a/chrome/browser/extensions/api/debugger/debugger_apitest.cc
+++ b/chrome/browser/extensions/api/debugger/debugger_apitest.cc
@@ -1150,7 +1150,6 @@
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
-// TODO(crbug.com/441339825): Fails on desktop Android.
// Tests that Target.createTarget to WebUI origins are blocked.
IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, CreateTargetToUntrustedWebUI) {
ASSERT_TRUE(RunExtensionTest("debugger_create_target_to_untrusted_webui"))
Original Bug Report
TargetHandlerAndroid::CreateTarget missing IsTrusted/MayReadLocalFiles check
Report description
TargetHandlerAndroid::CreateTarget missing IsTrusted/MayReadLocalFiles check
Bug location
Where do you want to report your vulnerability?
Chrome VRP β Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
The problem
Please describe the technical details of the vulnerability
The desktop fix at commit 28c26ea20357f (Bug 503197481, 2026-04-22, M138/M142/M143 LTS backports) added an
is_trusted_ / may_read_local_files_ check on Target.createTarget at
chrome/browser/devtools/protocol/target_handler.cc:168-178, rejecting devtools://, chrome-untrusted://, and
file:// URLs from untrusted CDP clients.
The Android handler at chrome/browser/devtools/protocol/target_handler_android.cc accepts the same two trust
parameters in its constructor signature but does not store them as member variables and does not apply
either check in CreateTarget. The URL flows from CDP directly into
tab_model->CreateNewTabForDevTools(GURL(url), β¦) with no scheme validation.
IsDomainAvailableToUntrustedClient<TargetHandlerAndroid>() returns true in
chrome/browser/devtools/protocol/chrome_devtools_session_android.cc:17, so the handler is allow-listed for
untrusted CDP clients (e.g., a remote-debugging client connected via adb forward).
Pre-flight grep 2026-05-11 against origin/main HEAD 06fb52aa1193a confirms the file has had zero
commits since the desktop fix (19 days untouched).
Vulnerable code
chrome/browser/devtools/protocol/target_handler_android.cc:14-18 β constructor accepts but discards both trust
parameters:
TargetHandlerAndroid::TargetHandlerAndroid(protocol::UberDispatcher* dispatcher,
bool is_trusted,
bool may_read_local_files) {
protocol::Target::Dispatcher::wire(dispatcher, this);
// is_trusted and may_read_local_files are accepted but not stored.
// No member fields exist for them. No later check on them in CreateTarget.
}
chrome/browser/devtools/protocol/target_handler_android.cc:38-77 β CreateTarget accepts the renderer-supplied url
and passes it straight through:
WebContents* web_contents =
tab_model->CreateNewTabForDevTools(GURL(url), new_window.value_or(false));
No scheme check anywhere in the function.
Compare to the desktop handler (already fixed)
chrome/browser/devtools/protocol/target_handler.cc:168-178, added by 28c26ea20357f:
if (!is_trusted_ && (inner_url.SchemeIs(content::kChromeUIUntrustedScheme) ||
inner_url.SchemeIs(content::kChromeDevToolsScheme))) {
return protocol::Response::ServerError(
"Navigating to a URL with a privileged scheme is not allowed");
}
if (!may_read_local_files_ && inner_url.SchemeIsFile()) {
return protocol::Response::ServerError(
"Creating a target with a local URL is not allowed");
}
The Android port was authored in 2025 with the same constructor signature (suggesting intent to apply the same
check) but the function body forgot to store or consume the parameters.
Live PoC evidence (Chrome 148.0.7778.120 stable on Android 11)
End-to-end repro against shipping Chrome stable. All three privileged URLs are accepted by Target.createTarget;
the desktop handler rejects all three:
URL: devtools://settings
Android (target_handler_android.cc, current): result.targetId = β¦ (accepted)
Desktop (target_handler.cc, fixed): error: "Navigating to a URL with a privileged scheme is not allowed"
ββββββββββββββββββββββββββββββββββββββββ
URL: chrome-untrusted://feedback
Android (target_handler_android.cc, current): result.targetId = β¦ (accepted)
Desktop (target_handler.cc, fixed): error: "Navigating to a URL with a privileged scheme is not allowed"
ββββββββββββββββββββββββββββββββββββββββ
URL: file:///sdcard/Documents/
Android (target_handler_android.cc, current): result.targetId = β¦ (accepted)
Desktop (target_handler.cc, fixed): error: "Creating a target with a local URL is not allowed"
The created tabs are real type:"page" entries in /json, each with its own per-target WebSocket debugger and
attachable from an untrusted CDP client.
Practical content render on Android Chrome 148 stable (verified via Runtime.evaluate of location.href and
document.body.innerText against each new target):
- file:///sdcard/Documents/ β renders Chrome's built-in directory-listing page (location.href =
file:///sdcard/Documents/, title = "Index of /sdcard/Documents/"). Attacker scrapes the listing via
Runtime.evaluate document.documentElement.outerHTML and enumerates every file name + size + timestamp visible to
the Chrome process. Page.captureScreenshot also returns the rendered listing as a PNG. Wire-confirmed.
- devtools://settings β location.href redirects to chrome-error://chromewebdata/, body reads "This site can't be
reached β¦ ERR_INVALID_URL". The Android Chrome stable build does not register a devtools://settings WebUI host, so
the renderer's URL validator rejects the URL after the CDP layer has already accepted it.
- chrome-untrusted://feedback β same chrome-error://chromewebdata/ redirect with ERR_INVALID_URL. Android does not
register this chrome-untrusted:// host either.
- Individual file:// file content read via direct navigation (e.g., file:///sdcard/Documents/poc09_sentinel.txt) β
renderer denies with ERR_ACCESS_DENIED (separate defense-in-depth, holds regardless of the CDP gap).
Conclusion on practical impact: the CDP-layer trust-check gap is real, and the rendering-side outcome on shipping
Chrome 148 stable is bounded by Chrome's renderer mitigations. The load-bearing exploitable capability today is
file:// directory enumeration. The protocol-layer fix is still required because (a) the desktop posture treats
devtools:// and chrome-untrusted:// rejection as a protocol-layer responsibility, and (b) any future Android
Chrome that adds new chrome-untrusted://... or devtools://... WebUI hosts becomes immediately exploitable without
this fix β the renderer's URL-validator-as-mitigation is brittle.
Threat model
- Caller: untrusted CDP client connected via adb forward from a desktop. The standard chrome://inspect developer
flow on Chrome for Android.
- Activation prereq on non-rooted device (honestly disclosed): the abstract socket chrome_devtools_remote requires
the user to have enabled chrome://flags/#enable-command-line-on-non-rooted-devices and written
--enable-remote-debugging to /data/local/tmp/chrome-command-line. This is the standard developer flow per
https://developer.chrome.com/docs/devtools/remote-debugging.
- Capability obtained: an untrusted CDP client can call Target.createTarget with a privileged-scheme or file://
URL and get back a targetId plus a per-target WebSocket. The new tab is attachable; the renderer renders content
for the file:// case and falls back to chrome-error://chromewebdata/ for the chrome-untrusted:// / devtools://
cases on current Android Chrome stable.
- Realistic attack scenarios: a user enables USB debugging for legitimate development/testing/screen-recording
reasons and runs an attacker-controlled tool on their desktop that uses the remote-debugging port; or another app
on the same machine reuses the chrome_devtools_remote abstract socket.
Suggested patch
Mirror the desktop handler's storage + check:
// target_handler_android.h β add member fields:
private:
bool is_trusted_ = false;
bool may_read_local_files_ = false;
// target_handler_android.cc β store in constructor and apply in CreateTarget:
TargetHandlerAndroid::TargetHandlerAndroid(
protocol::UberDispatcher* dispatcher,
bool is_trusted,
bool may_read_local_files)
: is_trusted_(is_trusted), may_read_local_files_(may_read_local_files) {
protocol::Target::Dispatcher::wire(dispatcher, this);
}
// In CreateTarget, before CreateNewTabForDevTools:
GURL gurl(url);
if (gurl.is_empty()) {
gurl = GURL(url::kAboutBlankURL);
}
GURL inner_url = gurl;
if (gurl.SchemeIs(content::kViewSourceScheme)) {
inner_url = GURL(gurl.GetContent());
}
if (!is_trusted_ && (inner_url.SchemeIs(content::kChromeUIUntrustedScheme) ||
inner_url.SchemeIs(content::kChromeDevToolsScheme))) {
return protocol::Response::ServerError(
"Navigating to a URL with a privileged scheme is not allowed");
}
if (!may_read_local_files_ && inner_url.SchemeIsFile()) {
return protocol::Response::ServerError(
"Creating a target with a local URL is not allowed");
}
Cross-reference
Sister-gap of:
- Bug 503197481 ("Disallow Target.createTarget to devtools:// URLs from untrusted clients") β commit 28c26ea20357f,
2026-04-22, with M138 / M142 / M143 LTS backports on the desktop handler.
The Android port of the same handler was authored in 2025 with a constructor signature accepting both is_trusted and
may_read_local_files β suggesting the author intended to apply the desktop check β but the function body never stores
or consumes either parameter. Pure forgotten-port shape.
#### Impact analysis
## Who can exploit
An **untrusted CDP client** connected via `adb forward` from a desktop to Chrome for Android's
`chrome_devtools_remote` abstract socket β the standard `chrome://inspect` developer flow. Examples:
- An attacker-controlled CLI that a user runs on their desktop while a Chrome-for-Android device is plugged in and
USB-debugging is enabled (a state developers, QA testers, and screen-recording users routinely have).
- Another app on the same desktop that reuses the `chrome_devtools_remote` abstract socket.
Untrusted-client reachability is confirmed by `IsDomainAvailableToUntrustedClient<TargetHandlerAndroid>()` returning
`true` (`chrome_devtools_session_android.cc:17`). The desktop fix (`28c26ea20357f`) treats exactly this caller as the
threat model.
## What they gain
### Protocol-layer (the bug)
`Target.createTarget` returns a real `targetId` for `devtools://`, `chrome-untrusted://`, and `file://` URLs that the
desktop handler now rejects. The new tab is a `type:"page"` entry in `/json` with its own per-target WebSocket
debugger, attachable by the same untrusted client. The CDP-layer trust check the desktop fix added is simply absent on
Android.
### Practical content render on Android Chrome 148 stable
Bounded by Chrome's renderer-side mitigations on the current build:
- **`file://` directory enumeration** β full DOM scrape of Chrome's built-in directory-listing page. Attacker reads
every file name + size + timestamp visible to the Chrome process via `Runtime.evaluate
document.documentElement.outerHTML`, and screenshots the listing via `Page.captureScreenshot`. Wire-confirmed.
- **`devtools://` / `chrome-untrusted://` content render** β renderer redirects to `chrome-error://chromewebdata/`
with `ERR_INVALID_URL` (those WebUI hosts aren't registered on Android stable). The renderer's URL validator
accidentally mitigates this half of the impact today.
- **Individual `file://` content read** β denied by renderer with `ERR_ACCESS_DENIED` (separate DiD).
### Forward-looking risk
Any future Android Chrome that registers additional `chrome-untrusted://...` or `devtools://...` WebUI hosts becomes
**immediately exploitable** without this fix. Chrome ships new internal pages every milestone; the renderer
URL-validator-as-mitigation is brittle. The protocol-layer fix is the correct posture and matches what the desktop fix
already established.
---
### The cause
#### What version of Chrome have you found the security issue in?
148.0.7778.120 stable (Chrome for Android, com.android.chrome)
#### Is the security issue related to a crash?
No, it is not related to a crash.
#### Choose the type of vulnerability
Privilege Escalation
#### How would you like to be publicly acknowledged for your report?
LegioSec