CVE-2026-12016
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/renderer_host/render_widget_host_impl.cc |
modified | |
ifcontent/browser/renderer_host/render_widget_host_impl.cc |
modified |
Files Changed
content/browser/devtools/protocol/input_handler.cccontent/browser/renderer_host/render_widget_host_impl.ccthird_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txtthird_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js
Patch
From 8c655d206d30a89ec85a3f4340a841f7e5a027e1 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
Date: Mon, 08 Jun 2026 08:45:21 -0700
Subject: [PATCH] Harden DevTools drag filtering
Ensure that drag events replayed via DevTools protocol (Input.dispatchDragEvent)
are properly sanitized by calling FilterDropData.
Also propagate FilterURL results back to the raw drag data in StartDragging
so that the intercepted event does not leak raw URLs.
Bug: 516482138
Change-Id: I008adc110b216e4dbd48774247e76d20b109a2e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7905536
Commit-Queue: Jonathan Ross <jonross@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1643233}
---
diff --git a/content/browser/devtools/protocol/input_handler.cc b/content/browser/devtools/protocol/input_handler.cc
index 9364acb..11dab51 100644
--- a/content/browser/devtools/protocol/input_handler.cc
+++ b/content/browser/devtools/protocol/input_handler.cc
@@ -1479,7 +1479,7 @@
static_cast<blink::DragOperationsMask>(data->GetDragOperationsMask());
std::unique_ptr<DropData> drop_data =
std::make_unique<DropData>(ProtocolDragDataToDropData(std::move(data)));
- drop_data->view_id = widget_host->GetRoutingID();
+ widget_host->FilterDropData(drop_data.get());
int event_modifiers =
GetEventModifiers(modifiers.value_or(blink::WebInputEvent::kNoModifiers),
false, false, 0, 0);
diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc
index 643dfb0..acebec73 100644
--- a/content/browser/renderer_host/render_widget_host_impl.cc
+++ b/content/browser/renderer_host/render_widget_host_impl.cc
@@ -2956,6 +2956,33 @@
}
}
+ // Propagate the FilterURL results back into `drag_data` so that the DevTools
+ // intercept path (Input.dragIntercepted) sees the same filtered values as the
+ // OS-drag path. Without this, a CDP client following the documented
+ // dragIntercepted -> dispatchDragEvent round-trip would replay the
+ // pre-filter renderer-supplied URLs into DragTargetDrop.
+ for (auto& item : drag_data->items) {
+ if (!item->is_string()) {
+ continue;
+ }
+ auto& s = item->get_string();
+ if (s->string_type == ui::kMimeTypeUriList) {
+ std::u16string rebuilt;
+ for (const auto& url_info : filtered_data.url_infos) {
+ if (!rebuilt.empty()) {
+ rebuilt.append(u"\r\n");
+ }
+ rebuilt.append(base::UTF8ToUTF16(url_info.url.spec()));
+ }
+ s->string_data = std::move(rebuilt);
+ } else if (s->string_type == ui::kMimeTypeHtml) {
+ s->base_url = filtered_data.html_base_url;
+ } else if (s->string_type == ui::kMimeTypeDownloadUrl &&
+ !filtered_data.download_metadata) {
+ s->string_data.clear();
+ }
+ }
+
// Filter out any paths that the renderer didn't have access to. This prevents
// the following attack on a malicious renderer:
// 1. StartDragging IPC sent with renderer-specified filesystem paths that it
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txt
new file mode 100644
index 0000000..2549428
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txt
@@ -0,0 +1,3 @@
+Tests that Input.dispatchDragEvent sanitizes URLs (FilterURL) when dropping onto a web page.
+Dropped URI: about:blank#blocked
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js
new file mode 100644
index 0000000..bc6daf7
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js
@@ -0,0 +1,50 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+ const {page, session, dp} = await testRunner.startHTML(`
+ <div id="drop-target" style="width: 100px; height: 100px; background: blue"></div>
+ <script>
+ window.dropped_uri = null;
+ const div = document.getElementById('drop-target');
+ div.addEventListener('dragenter', e => e.preventDefault());
+ div.addEventListener('dragover', e => e.preventDefault());
+ div.addEventListener('drop', e => {
+ e.preventDefault();
+ window.dropped_uri = e.dataTransfer.getData('text/uri-list');
+ });
+ </script>
+ `, `Tests that Input.dispatchDragEvent sanitizes URLs (FilterURL) when dropping onto a web page.`);
+
+ const data = {
+ items: [
+ {
+ mimeType: 'text/uri-list',
+ data: 'file:///etc/passwd',
+ }
+ ],
+ dragOperationsMask: 1,
+ };
+
+ await dp.Input.dispatchDragEvent({
+ type: 'dragEnter',
+ data,
+ x: 50,
+ y: 50,
+ });
+
+ await dp.Input.dispatchDragEvent({
+ type: 'dragOver',
+ data,
+ x: 50,
+ y: 50,
+ });
+
+ await dp.Input.dispatchDragEvent({
+ type: 'drop',
+ data,
+ x: 50,
+ y: 50,
+ });
+
+ const droppedUri = await session.evaluate(`window.dropped_uri`);
+ testRunner.log('Dropped URI: ' + droppedUri);
+ testRunner.completeTest();
+})
Regression Test / PoC
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txt
new file mode 100644
index 0000000..2549428
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txt
@@ -0,0 +1,3 @@
+Tests that Input.dispatchDragEvent sanitizes URLs (FilterURL) when dropping onto a web page.
+Dropped URI: about:blank#blocked
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js
new file mode 100644
index 0000000..bc6daf7
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js
@@ -0,0 +1,50 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+ const {page, session, dp} = await testRunner.startHTML(`
+ <div id="drop-target" style="width: 100px; height: 100px; background: blue"></div>
+ <script>
+ window.dropped_uri = null;
+ const div = document.getElementById('drop-target');
+ div.addEventListener('dragenter', e => e.preventDefault());
+ div.addEventListener('dragover', e => e.preventDefault());
+ div.addEventListener('drop', e => {
+ e.preventDefault();
+ window.dropped_uri = e.dataTransfer.getData('text/uri-list');
+ });
+ </script>
+ `, `Tests that Input.dispatchDragEvent sanitizes URLs (FilterURL) when dropping onto a web page.`);
+
+ const data = {
+ items: [
+ {
+ mimeType: 'text/uri-list',
+ data: 'file:///etc/passwd',
+ }
+ ],
+ dragOperationsMask: 1,
+ };
+
+ await dp.Input.dispatchDragEvent({
+ type: 'dragEnter',
+ data,
+ x: 50,
+ y: 50,
+ });
+
+ await dp.Input.dispatchDragEvent({
+ type: 'dragOver',
+ data,
+ x: 50,
+ y: 50,
+ });
+
+ await dp.Input.dispatchDragEvent({
+ type: 'drop',
+ data,
+ x: 50,
+ y: 50,
+ });
+
+ const droppedUri = await session.evaluate(`window.dropped_uri`);
+ testRunner.log('Dropped URI: ' + droppedUri);
+ testRunner.completeTest();
+})
Original Bug Report
Potential FilterURL bypass in RenderWidgetHostImpl::StartDragging via DevTools drag interception
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A compromised renderer process can potentially bypass browser-side FilterURL security checks when a DevTools Protocol (CDP) session with drag interception is active. By capitalizing on DevTools serializing raw, unfiltered DragData rather than the sanitized copy, an attacker can replay events to inject restricted URLs. On ChromeOS, this can grant unauthorized file access and commit privileges to the targeted process.
Affected files:
content/browser/renderer_host/render_widget_host_impl.cccontent/browser/devtools/protocol/input_handler.cccontent/browser/devtools/devtools_instrumentation.cccontent/browser/file_system/browser_file_system_helper.cc
Estimated timestamp from git blame: 2021-04-07
Description
A potential browser-process logic flaw exists in the DevTools drag-and-drop interception pipeline. When a drag operation is initiated, RenderWidgetHostImpl::StartDragging sanitizes renderer-provided URLs on a copy (filtered_data) but leaves the original drag_data unmodified. If a DevTools session has enabled drag interception via Input.setInterceptDrags, this raw, unsanitized drag_data is serialized and sent to the client. Replaying this intercepted event allows the client to bypass standard drop sanitization checks, which can lead to privilege escalation (e.g., unauthorized file access on ChromeOS).
Root Cause Analysis
-
Unfiltered Data Forwarded: In
content/browser/renderer_host/render_widget_host_impl.cc(line 2908),StartDraggingcreates a sanitized copy of the drag data:DropData drop_data = DragDataToDropData(*drag_data); DropData filtered_data(drop_data); // ... FilterURL applied to filtered_data ...However, the original, raw
drag_datais forwarded directly to DevTools instrumentation without being sanitized (line 2980):devtools_instrumentation::WillStartDragging( frame_tree_->root(), filtered_data, std::move(drag_data), drag_operations_mask, &intercepted); -
Raw Serialization: In
content/browser/devtools/protocol/input_handler.cc(line 1573), when drag interception is active,InputHandler::StartDraggingserializes the raw, unfiltered string items insidedrag_data.itemsinstead of using the sanitizedfiltered_data(line 1595):for (const auto& item : drag_data.items) { if (!item->is_string()) continue; const auto& string_item = item->get_string(); auto protocol_item = protocol::Input::DragDataItem::Create() .SetMimeType(string_item->string_type) .SetData(base::UTF16ToUTF8(string_item->string_data)) // Raw, unsanitized URL .Build(); -
Sanitization Bypass on Replay: When the CDP client replays the intercepted event using
Input.dispatchDragEvent(line 1432), the incoming client payload is translated back to aDropDataobject. The function directly executeswidget_host->DragTargetDrop(line 1519) without callingRenderWidgetHostImpl::FilterDropData(which is typically called on OS-level drag inputs to sanitize incoming schemas). -
Privilege Escalation: On ChromeOS, this reaches
PrepareDropDataForChildProcess(content/browser/file_system/browser_file_system_helper.cc, line 212). If the unsanitizedDropDatacontains anexternalfile://scheme, the browser grants commit and read access to the target renderer process (line 235):security_policy->GrantCommitURL(child_id.GetUnsafeValue(), url);
Potential Steps to Trigger
Note: These are potential steps based on source code analysis; our tools have not executed this flow dynamically.
- A DevTools client session connects and enables drag interception by issuing the command
Input.setInterceptDragswith{enabled: true}. - A compromised renderer process initiates a drag action, passing a restricted scheme (e.g.,
externalfile://drive/root/secret) insideblink::mojom::DragData. - The browser filters the sanitized copy but emits the raw, unfiltered scheme to the DevTools frontend inside the
Input.dragInterceptedevent. - The DevTools client (acting as a confused deputy) replays the event back to the browser via
Input.dispatchDragEvent. - The browser processes the dropped data without executing standard
FilterDropDatachecks, triggeringPrepareDropDataForChildProcessand granting unauthorized commit and read permissions for the target URL to the renderer process.
Suggested Remediation
To resolve this issue, DevTools drag-and-drop instrumentation should serialize data exclusively from the sanitized filtered_data structure inside InputHandler::StartDragging instead of accessing the raw, renderer-supplied drag_data.items. Alternatively, ensure that the drag event replay path (InputHandler::DispatchDragEvent) executes standard FilterDropData verification to sanitize incoming URLs before forwarding them to DragTargetDrop.
Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.