Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input DevTools
DescriptionInsufficient validation of untrusted input DevTools
ComponentChromium
Bug ClassLogic Error
Tracker516482138
Fix commit8c655d206d30 (chromium/src) +81/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-11

Changed Functions

FunctionChangeNotes
for
content/browser/renderer_host/render_widget_host_impl.cc
modified
if
content/browser/renderer_host/render_widget_host_impl.cc
modified

Files Changed

  • content/browser/devtools/protocol/input_handler.cc
  • content/browser/renderer_host/render_widget_host_impl.cc
  • third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize-expected.txt
  • third_party/blink/web_tests/http/tests/inspector-protocol/input/dispatch-drag-event-sanitize.js
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();
+})
Loading diff…

Regression Test / PoC

shipped with the fix
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();
+})
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • content/browser/devtools/protocol/input_handler.cc
  • content/browser/devtools/devtools_instrumentation.cc
  • content/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

  1. Unfiltered Data Forwarded: In content/browser/renderer_host/render_widget_host_impl.cc (line 2908), StartDragging creates 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_data is 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);
    
  2. Raw Serialization: In content/browser/devtools/protocol/input_handler.cc (line 1573), when drag interception is active, InputHandler::StartDragging serializes the raw, unfiltered string items inside drag_data.items instead of using the sanitized filtered_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();
    
  3. 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 a DropData object. The function directly executes widget_host->DragTargetDrop (line 1519) without calling RenderWidgetHostImpl::FilterDropData (which is typically called on OS-level drag inputs to sanitize incoming schemas).

  4. Privilege Escalation: On ChromeOS, this reaches PrepareDropDataForChildProcess (content/browser/file_system/browser_file_system_helper.cc, line 212). If the unsanitized DropData contains an externalfile:// 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.

  1. A DevTools client session connects and enables drag interception by issuing the command Input.setInterceptDrags with {enabled: true}.
  2. A compromised renderer process initiates a drag action, passing a restricted scheme (e.g., externalfile://drive/root/secret) inside blink::mojom::DragData.
  3. The browser filters the sanitized copy but emits the raw, unfiltered scheme to the DevTools frontend inside the Input.dragIntercepted event.
  4. The DevTools client (acting as a confused deputy) replays the event back to the browser via Input.dispatchDragEvent.
  5. The browser processes the dropped data without executing standard FilterDropData checks, triggering PrepareDropDataForChildProcess and 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.

View on issue tracker