Critical chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in DataTransfer
DescriptionInsufficient validation of untrusted input in DataTransfer
ComponentDataTransfer
Bug ClassLogic Error
Tracker496393078
Fix commit988ac5cb880b (chromium/src) +609/-358
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Files Changed

  • components/download/public/common/download_url_parameters.h
  • content/app_shim_remote_cocoa/web_contents_ns_view_bridge.h
  • content/app_shim_remote_cocoa/web_contents_ns_view_bridge.mm
  • content/app_shim_remote_cocoa/web_contents_view_cocoa.h
  • content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
  • content/app_shim_remote_cocoa/web_drag_source_mac.h
  • content/app_shim_remote_cocoa/web_drag_source_mac.mm
From 988ac5cb880ba06ce8ddd8fe6ebc60dab03ec972 Mon Sep 17 00:00:00 2001
From: Daniel Cheng <[email protected]>
Date: Wed, 01 Apr 2026 14:24:12 -0700
Subject: [PATCH] Filter DownloadURL when starting drags from Blink

RenderWidgetHostImpl::StartDragging() already filters other fields that
contain URLs, but DownloadURL was previously missed since it wasn't
parsed until later, inside RenderWidgetHostDelegateView. The primary
change here consist of several parts:

1. Change `content::DropData` to hold the optional, already-parsed
   DownloadURL metadata and move the parsing earlier on the browser
   side. This is technically a rule of 2 violation, but it is
   pre-existing and the parser itself is simple enough. Ideally, the
   renderer would pass the already-parsed form over IPC, but that
   doesn't quite fit with how drag items are represented today.
   Refactoring this is left for a followup in
   https://crbug.com/497928951.

2. Change `RenderWidgetHostImpl::StartDrag()` to actually filter the
   URL, and to reset the download URL metadata to be empty if it fails
   the check.

3. Add some unit tests to ensure this filtering actually happens.

The other part of this change fixes a long-standing bug to use the
source RenderFrameHost rather than always using the primary main frame.
Where possible, functions have been simplified to avoid passing
redundant parameters, e.g. `RenderWidgetHostImpl::StartDragging()` only
takes a source RenderFrameHost now, since the initiator origin and the
source RenderWidgetHost can both be derived from the source
RenderFrameHost. This plumbing ends up being somewhat involved, due to
various platform-specific quirks:

1. `DragDownloadFile`'s constructor takes a `WeakDocumentPtr` instead of
   a `RenderFrameHost&` or `GlobalRenderFrameHostToken`, since this
   better matches the desired semantics of RenderDocument.

2. On Mac, drags may need to be proxied to the out-of-process app shim.
   Pointers should not be passed between processes, so pass a child
   process ID and document token pair, which requires adding Mojo
   plumbing and a new `RenderFrameHostImpl::FromDocumentToken()`
   overload that takes a ChildProcessID. The existing overload is marked
   as deprecated; followups will remove use of the deprecated overload.

3. On Android, InputOnViz adds another wrinkle since there are times
   that the browser may need to request input back from Viz–so drags may
   start asynchronously. Similarly, plumb through a WeakDocumentPtr
   here to match the desired semantics of RenderDocument.

gemini-cli was used to debug and implement portions of tests, as well as
add boilerplate plumbing, e.g. for Mojo.

Bug: 40470366, 496393078
Change-Id: I747e1d472888f0f93219dd194b8d49dea8b4dffe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7708800
Reviewed-by: Kartar Singh <[email protected]>
Reviewed-by: Min Qin <[email protected]>
Commit-Queue: Daniel Cheng <[email protected]>
Reviewed-by: Charlie Reis <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1608777}
---

diff --git a/components/download/public/common/download_url_parameters.h b/components/download/public/common/download_url_parameters.h
index 1e640c02..1e71a0bf 100644
--- a/components/download/public/common/download_url_parameters.h
+++ b/components/download/public/common/download_url_parameters.h
@@ -123,8 +123,8 @@
 
   // The origin of the context which initiated the request. See
   // net::URLRequest::initiator().
-  void set_initiator(const std::optional<url::Origin>& initiator) {
-    initiator_ = initiator;
+  void set_initiator(std::optional<url::Origin> initiator) {
+    initiator_ = std::move(initiator);
   }
 
   // If this is a request for resuming an HTTP/S download, |last_modified|
diff --git a/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.h b/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.h
index 3e4bdae..096cfb5 100644
--- a/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.h
+++ b/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.h
@@ -58,8 +58,10 @@
   void SetVisible(bool visible) override;
   void MakeFirstResponder() override;
   void TakeFocus(bool reverse) override;
-  void StartDrag(const content::DropData& drop_data,
+  void StartDrag(content::ChildProcessId render_process_id,
+                 const blink::DocumentToken& document_token,
                  const url::Origin& source_origin,
+                 const content::DropData& drop_data,
                  uint32_t operation_mask,
                  const gfx::ImageSkia& image,
                  const gfx::Vector2d& image_offset,
diff --git a/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.mm b/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.mm
index d9cbcd75..91b83cc 100644
--- a/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.mm
+++ b/content/app_shim_remote_cocoa/web_contents_ns_view_bridge.mm
@@ -123,14 +123,20 @@
     [[ns_view_ window] selectNextKeyView:ns_view_];
 }
 
-void WebContentsNSViewBridge::StartDrag(const content::DropData& drop_data,
-                                        const url::Origin& source_origin,
-                                        uint32_t operation_mask,
-                                        const gfx::ImageSkia& image,
-                                        const gfx::Vector2d& image_offset,
-                                        bool is_privileged) {
+void WebContentsNSViewBridge::StartDrag(
+    content::ChildProcessId render_process_id,
+    const blink::DocumentToken& document_token,
+    const url::Origin& source_origin,
+    const content::DropData& drop_data,
+    uint32_t operation_mask,
+    const gfx::ImageSkia& image,
+    const gfx::Vector2d& image_offset,
+    bool is_privileged) {
   NSPoint offset = gfx::PointAtOffsetFromOrigin(image_offset).ToCGPoint();
+  // TODO(dcheng): Check if is_privileged still needs to be passed here.
   [ns_view_ startDragWithDropData:drop_data
+                  renderProcessId:render_process_id
+                    documentToken:document_token
                      sourceOrigin:source_origin
                 dragOperationMask:operation_mask
                             image:gfx::NSImageFromImageSkia(image)
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.h b/content/app_shim_remote_cocoa/web_contents_view_cocoa.h
index 85bc3fad..bf346098 100644
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.h
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.h
@@ -8,6 +8,7 @@
 #include "base/memory/raw_ptr.h"
 #include "content/common/content_export.h"
 #include "content/common/web_contents_ns_view_bridge.mojom.h"
+#include "content/public/common/child_process_id.h"
 #import "ui/base/cocoa/base_view.h"
 #import "ui/base/cocoa/views_hostable.h"
 
@@ -44,6 +45,8 @@
 - (instancetype)initWithViewsHostableView:(ui::ViewsHostableView*)v;
 - (void)registerDragTypes;
 - (void)startDragWithDropData:(const content::DropData&)dropData
+              renderProcessId:(content::ChildProcessId)renderProcessId
+                documentToken:(const blink::DocumentToken&)documentToken
                  sourceOrigin:(const url::Origin&)sourceOrigin
             dragOperationMask:(NSDragOperation)operationMask
                         image:(NSImage*)image
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
index 1ef2c90..da2786e0 100644
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
@@ -224,6 +224,8 @@
 }
 
 - (void)startDragWithDropData:(const DropData&)dropData
+              renderProcessId:(content::ChildProcessId)renderProcessId
+                documentToken:(const blink::DocumentToken&)documentToken
                  sourceOrigin:(const url::Origin&)sourceOrigin
             dragOperationMask:(NSDragOperation)operationMask
                         image:(NSImage*)image
@@ -244,8 +246,10 @@
                                           pressure:1.0];
 
   _dragSource = [[WebDragSource alloc] initWithHost:_host
-                                           dropData:dropData
+                                    renderProcessId:renderProcessId
+                                      documentToken:documentToken
                                        sourceOrigin:sourceOrigin
+                                           dropData:dropData
                                        isPrivileged:isPrivileged];
   NSDraggingItem* draggingItem =
       [[NSDraggingItem alloc] initWithPasteboardWriter:_dragSource];
diff --git a/content/app_shim_remote_cocoa/web_drag_source_mac.h b/content/app_shim_remote_cocoa/web_drag_source_mac.h
index 0a2ce41f..59c5a39 100644
--- a/content/app_shim_remote_cocoa/web_drag_source_mac.h
+++ b/content/app_shim_remote_cocoa/web_drag_source_mac.h
@@ -10,7 +10,9 @@
 #include "base/files/file_path.h"
 #include "base/memory/raw_ptr.h"
 #include "content/common/content_export.h"
+#include "content/public/common/child_process_id.h"
 #include "content/public/common/drop_data.h"
+#include "third_party/blink/public/common/tokens/tokens.h"
 
 namespace content {
 struct DropData;
@@ -31,8 +33,10 @@
 
 // Initialize a WebDragSource object for a drag.
 - (instancetype)initWithHost:(remote_cocoa::mojom::WebContentsNSViewHost*)host
-                    dropData:(const content::DropData&)dropData
+             renderProcessId:(content::ChildProcessId)renderProcessId
+               documentToken:(const blink::DocumentToken&)documentToken
                 sourceOrigin:(const url::Origin&)sourceOrigin
+                    dropData:(const content::DropData&)dropData
                 isPrivileged:(BOOL)privileged;
 
 // Call when the WebContents is gone.
diff --git a/content/app_shim_remote_cocoa/web_drag_source_mac.mm b/content/app_shim_remote_cocoa/web_drag_source_mac.mm
index 55cfdf77..e7bf29bc 100644
--- a/content/app_shim_remote_cocoa/web_drag_source_mac.mm
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/app_shim_remote_cocoa/window_occlusion_browsertest_mac.mm b/content/app_shim_remote_cocoa/window_occlusion_browsertest_mac.mm
index b40cfd9d..0ef8bdf 100644
--- a/content/app_shim_remote_cocoa/window_occlusion_browsertest_mac.mm
+++ b/content/app_shim_remote_cocoa/window_occlusion_browsertest_mac.mm
@@ -306,18 +306,18 @@
   void PerformDragOperation(DraggingInfoPtr dragging_info,
                             PerformDragOperationCallback callback) override {}
 
-  bool DragPromisedFileTo(const ::base::FilePath& file_path,
+  bool DragPromisedFileTo(content::ChildProcessId render_process_id,
+                          const blink::DocumentToken& document_token,
+                          const ::base::FilePath& file_path,
                           const ::content::DropData& drop_data,
-                          const ::GURL& download_url,
-                          const ::url::Origin& source_origin,
                           ::base::FilePath* out_file_path) override {
     return false;
   }
 
-  void DragPromisedFileTo(const ::base::FilePath& file_path,
+  void DragPromisedFileTo(content::ChildProcessId render_process_id,
+                          const blink::DocumentToken& document_token,
+                          const ::base::FilePath& file_path,
                           const ::content::DropData& drop_data,
-                          const ::GURL& download_url,
-                          const ::url::Origin& source_origin,
                           DragPromisedFileToCallback callback) override {}
 
   void EndDrag(uint32_t drag_operation,
diff --git a/content/browser/download/drag_download_file_browsertest.cc b/content/browser/download/drag_download_file_browsertest.cc
index 279d091..dc97f7b 100644
--- a/content/browser/download/drag_download_file_browsertest.cc
+++ b/content/browser/download/drag_download_file_browsertest.cc
@@ -15,10 +15,12 @@
 #include "base/threading/thread_restrictions.h"
 #include "content/browser/download/download_manager_impl.h"
 #include "content/browser/download/drag_download_util.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/content_browser_client.h"
+#include "content/public/browser/render_process_host.h"
 #include "content/public/common/content_client.h"
 #include "content/public/common/content_paths.h"
 #include "content/public/test/browser_test.h"
@@ -103,8 +105,8 @@
   Referrer referrer;
   std::string referrer_encoding;
   auto file = std::make_unique<DragDownloadFile>(
-      name, base::File(), url, referrer, referrer_encoding, std::nullopt,
-      shell()->web_contents());
+      shell()->web_contents()->GetPrimaryMainFrame()->GetWeakDocumentPtr(),
+      name, base::File(), url, referrer, referrer_encoding);
   scoped_refptr<MockDownloadFileObserver> observer(
       new MockDownloadFileObserver());
   EXPECT_CALL(*observer.get(), OnDownloadAborted())
@@ -122,8 +124,8 @@
   Referrer referrer;
   std::string referrer_encoding;
   auto file = std::make_unique<DragDownloadFile>(
-      name, base::File(), url, referrer, referrer_encoding, std::nullopt,
-      shell()->web_contents());
+      shell()->web_contents()->GetPrimaryMainFrame()->GetWeakDocumentPtr(),
+      name, base::File(), url, referrer, referrer_encoding);
   scoped_refptr<MockDownloadFileObserver> observer(
       new MockDownloadFileObserver());
   EXPECT_CALL(*observer.get(), OnDownloadCompleted(_))
@@ -138,13 +140,11 @@
   base::FilePath name(
       downloads_directory().AppendASCII("DragDownloadFileTest_Initiator.txt"));
   GURL url = embedded_test_server()->GetURL("/echoheader?sec-fetch-site");
-  url::Origin initiator =
-      url::Origin::Create(GURL("https://initiator.example.com"));
   Referrer referrer;
   std::string referrer_encoding;
   auto file = std::make_unique<DragDownloadFile>(
-      name, base::File(), url, referrer, referrer_encoding, initiator,
-      shell()->web_contents());
+      shell()->web_contents()->GetPrimaryMainFrame()->GetWeakDocumentPtr(),
+      name, base::File(), url, referrer, referrer_encoding);
   base::FilePath downloaded_path;
   scoped_refptr<MockDownloadFileObserver> observer(
       new MockDownloadFileObserver());
@@ -174,8 +174,8 @@
   Referrer referrer;
   std::string referrer_encoding;
   auto file = std::make_unique<DragDownloadFile>(
-      name, base::File(), url, referrer, referrer_encoding, std::nullopt,
-      shell()->web_contents());
+      shell()->web_contents()->GetPrimaryMainFrame()->GetWeakDocumentPtr(),
+      name, base::File(), url, referrer, referrer_encoding);
   scoped_refptr<MockDownloadFileObserver> observer(
       new MockDownloadFileObserver());
   ON_CALL(*observer.get(), OnDownloadAborted())
diff --git a/content/browser/renderer_host/render_view_host_unittest.cc b/content/browser/renderer_host/render_view_host_unittest.cc
index 5d6e974..9ff6cb1 100644
--- a/content/browser/renderer_host/render_view_host_unittest.cc
+++ b/content/browser/renderer_host/render_view_host_unittest.cc
@@ -85,14 +85,14 @@
     : public RenderViewHostDelegateView {
  public:
   ~MockDraggingRenderViewHostDelegateView() override {}
-  void StartDragging(const DropData& drop_data,
-                     const url::Origin& source_origin,
-                     blink::DragOperationsMask allowed_ops,
-                     const gfx::ImageSkia& image,
-                     const gfx::Vector2d& cursor_offset,
-                     const gfx::Rect& drag_obj_rect,
-                     const blink::mojom::DragEventSourceInfo& event_info,
-                     RenderWidgetHostImpl* source_rwh) override {
+  void StartDragging(
+      RenderFrameHost& source_rfh,
+      const DropData& drop_data,
+      blink::DragOperationsMask allowed_ops,
+      const gfx::ImageSkia& image,
+      const gfx::Vector2d& cursor_offset,
+      const gfx::Rect& drag_obj_rect,
+      const blink::mojom::DragEventSourceInfo& event_info) override {
     drag_url_ = drop_data.url_infos.front().url;
     html_base_url_ = drop_data.html_base_url;
   }
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index 631f858..db5e430 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -46,6 +46,7 @@
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/common/drop_data.h"
+#include "content/public/common/url_constants.h"
 #include "content/public/test/browser_task_environment.h"
 #include "content/public/test/mock_render_process_host.h"
 #include "content/public/test/test_browser_context.h"
@@ -55,6 +56,7 @@
 #include "content/test/stub_render_widget_host_owner_delegate.h"
 #include "content/test/test_render_view_host.h"
 #include "content/test/test_render_widget_host.h"
+#include "content/test/test_web_contents.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -68,6 +70,7 @@
 #include "third_party/blink/public/mojom/drag/drag.mojom.h"
 #include "third_party/blink/public/mojom/input/input_handler.mojom-shared.h"
 #include "third_party/blink/public/mojom/input/touch_event.mojom.h"
+#include "ui/base/clipboard/clipboard_constants.h"
 #include "ui/base/cursor/cursor.h"
 #include "ui/display/display_util.h"
 #include "ui/display/screen.h"
@@ -284,21 +287,24 @@
   ~MockRenderViewHostDelegateView() override = default;
 
   int start_dragging_count() const { return start_dragging_count_; }
+  const DropData& drop_data() const { return drop_data_; }
 
   // RenderViewHostDelegateView:
-  void StartDragging(const DropData& drop_data,
-                     const url::Origin& source_origin,
-                     blink::DragOperationsMask allowed_ops,
-                     const gfx::ImageSkia& image,
-                     const gfx::Vector2d& cursor_offset,
-                     const gfx::Rect& drag_obj_rect,
-                     const blink::mojom::DragEventSourceInfo& event_info,
-                     RenderWidgetHostImpl* source_rwh) override {
+  void StartDragging(
+      RenderFrameHost& source_rfh,
+      const DropData& drop_data,
+      blink::DragOperationsMask allowed_ops,
+      const gfx::ImageSkia& image,
+      const gfx::Vector2d& cursor_offset,
+      const gfx::Rect& drag_obj_rect,
+      const blink::mojom::DragEventSourceInfo& event_info) override {
     ++start_dragging_count_;
+    drop_data_ = drop_data;
   }
 
  private:
   int start_dragging_count_ = 0;
+  DropData drop_data_;
 };
 
 // FakeRenderFrameMetadataObserver -----------------------------------------
@@ -2264,38 +2270,141 @@
             visual_properties.compositor_viewport_pixel_rect);
 }
 
-// Make sure no dragging occurs after renderer exited. See crbug.com/704832.
-TEST_F(RenderWidgetHostTest, RendererExitedNoDrag) {
-  host_->SetView(new TestView(host_.get()));
+class DragTestContentBrowserClient : public ContentBrowserClient {
+ public:
+  // The default implementation returns `false`, but this means that
+  // `CanRequestURL()` for a URL with a file scheme ends up returning true,
+  // since `ChildProcessSecurityPolicy` assumes that unhandled schemes are
+  // external protocols.
+  bool IsHandledURL(const GURL& url) override {
+    return url.SchemeIs(url::kFileScheme);
+  }
+};
 
-  EXPECT_EQ(delegate_->mock_delegate_view()->start_dragging_count(), 0);
+class RenderWidgetHostDragTest : public RenderViewHostImplTestHarness {
+ public:
+  RenderWidgetHostDragTest() {
+    old_browser_client_ = SetBrowserClientForTesting(&drag_browser_client_);
+  }
+
+  ~RenderWidgetHostDragTest() override {
+    SetBrowserClientForTesting(old_browser_client_);
+  }
+
+  void SetUp() override {
+    RenderViewHostImplTestHarness::SetUp();
+    contents()->set_delegate_view(&mock_delegate_view_);
+    main_test_rfh()->InitializeRenderFrameIfNeeded();
+  }
+
+  void StartDragWithDropData(const DropData& drop_data) {
+    StartDragWithDragData(
+        DropDataToDragData(drop_data, GetFileSystemAccessManager(),
+                           main_test_rfh()->GetProcess()->GetDeprecatedID(),
+                           GetChromeBlobStorageContext()));
+  }
+
+  void StartDragWithDragData(blink::mojom::DragDataPtr drag_data) {
+    GetRenderWidgetHost()->StartDragging(
+        *main_test_rfh(), std::move(drag_data), blink::kDragOperationEvery,
+        SkBitmap(), gfx::Vector2d(), gfx::Rect(),
+        blink::mojom::DragEventSourceInfo::New());
+  }
+
+  RenderWidgetHostImpl* GetRenderWidgetHost() {
+    return static_cast<RenderWidgetHostImpl*>(
+        main_test_rfh()->GetRenderWidgetHost());
+  }
+
+  FileSystemAccessManagerImpl* GetFileSystemAccessManager() {
+    return static_cast<StoragePartitionImpl*>(
+               contents()->GetBrowserContext()->GetDefaultStoragePartition())
+        ->GetFileSystemAccessManager();
+  }
+
+  scoped_refptr<ChromeBlobStorageContext> GetChromeBlobStorageContext() {
+    return ChromeBlobStorageContext::GetFor(contents()->GetBrowserContext());
+  }
+
+  int start_dragging_count() const {
+    return mock_delegate_view_.start_dragging_count();
+  }
+
+  const DropData& drop_data() const { return mock_delegate_view_.drop_data(); }
+
+ private:
+  DragTestContentBrowserClient drag_browser_client_;
+  raw_ptr<ContentBrowserClient> old_browser_client_;
+  MockRenderViewHostDelegateView mock_delegate_view_;
+};
+
+// Make sure no dragging occurs after renderer exited. See crbug.com/704832.
+TEST_F(RenderWidgetHostDragTest, RendererExitedNoDrag) {
+  EXPECT_EQ(start_dragging_count(), 0);
 
   GURL http_url = GURL("http://www.domain.com/index.html");
   DropData drop_data;
   drop_data.url_infos = {ui::ClipboardUrlInfo{http_url, u""}};
   drop_data.html_base_url = http_url;
-  FileSystemAccessManagerImpl* file_system_manager =
-      static_cast<StoragePartitionImpl*>(process_->GetStoragePartition())
-          ->GetFileSystemAccessManager();
-  blink::DragOperationsMask drag_operation = blink::kDragOperationEvery;
-  host_->StartDragging(
-      DropDataToDragData(
-          drop_data, file_system_manager, process_->GetDeprecatedID(),
-          ChromeBlobStorageContext::GetFor(process_->GetBrowserContext())),
-      url::Origin(), drag_operation, SkBitmap(), gfx::Vector2d(), gfx::Rect(),
-      blink::mojom::DragEventSourceInfo::New());
-  EXPECT_EQ(delegate_->mock_delegate_view()->start_dragging_count(), 1);
+
+  StartDragWithDropData(drop_data);
+  EXPECT_EQ(start_dragging_count(), 1);
 
   // Simulate that renderer exited due navigation to the next page.
-  host_->RendererExited();
-  EXPECT_FALSE(host_->GetView());
-  host_->StartDragging(
-      DropDataToDragData(
-          drop_data, file_system_manager, process_->GetDeprecatedID(),
-          ChromeBlobStorageContext::GetFor(process_->GetBrowserContext())),
-      url::Origin(), drag_operation, SkBitmap(), gfx::Vector2d(), gfx::Rect(),
-      blink::mojom::DragEventSourceInfo::New());
-  EXPECT_EQ(delegate_->mock_delegate_view()->start_dragging_count(), 1);
+  GetRenderWidgetHost()->RendererExited();
+  EXPECT_FALSE(GetRenderWidgetHost()->GetView());
+
+  StartDragWithDropData(drop_data);
+  EXPECT_EQ(start_dragging_count(), 1);
+}
+
+TEST_F(RenderWidgetHostDragTest, NonFileUrlSpecifiesDownloadUrlWithFileUrl) {
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Potential Local File Disclosure via Drag-and-Drop Frame Confusion

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A malicious subframe embedded in a privileged page can bypass process isolation to steal local files via HTML5 drag-and-drop. This occurs due to missing IPC validation of download_metadata in StartDragging and frame confusion in DragDownloadFile, which incorrectly evaluates download permissions using the primary main frame’s privileges.

Affected files:

  • content/browser/renderer_host/render_widget_host_impl.cc
  • content/browser/download/drag_download_file.cc

Estimated timestamp from git blame: 2026-01-23

Summary

A potential security gap exists in the handling of drag-and-drop operations, allowing an unprivileged subframe to steal local files or access restricted URLs if embedded within a privileged main frame (such as a file:// page or an extension). The issue stems from a combination of missing IPC validation for drag metadata and a frame confusion bug where the browser uses the primary main frame’s process ID for security checks instead of the actual IPC sender’s process ID.

Technical Details

There are two interlinked flaws that make this attack possible:

  1. Missing IPC Validation in StartDragging In content/browser/renderer_host/render_widget_host_impl.cc, the StartDragging IPC handler converts blink::mojom::DragData into DropData. It performs ChildProcessSecurityPolicy checks and FilterURL filtering on various fields (like url_infos, filenames, and html_base_url). However, it completely fails to validate or apply FilterURL to drop_data.download_metadata. This allows a compromised or malicious renderer to pass highly privileged URLs (e.g., file:///C:/secret.txt) across the IPC boundary.

  2. Frame Confusion in DragDownloadFile When the drag operation converts the metadata into a download, a DragDownloadFile object is created (content/browser/download/drag_download_file.cc). Its constructor explicitly uses web_contents->GetPrimaryMainFrame() to determine the render_process_id and render_frame_id:

RenderFrameHost* host = web_contents->GetPrimaryMainFrame();
drag_ui_ = new DragDownloadFileUI(
    url, referrer, referrer_encoding, initiator_origin,
    host->GetProcess()->GetDeprecatedID(), host->GetRoutingID(),
    ...);

When the drop triggers the download, DownloadManagerImpl::BeginDownloadInternal evaluates the request’s safety via DownloadRequestUtils::IsURLSafe. Because DragDownloadFile provided the primary main frame’s process ID, ChildProcessSecurityPolicy::CanRequestURL evaluates the file access against the main frame’s permissions. If the main frame is a file:// document, the check passes, and the browser copies the local file to a temporary drag directory.

When the drop completes over the attacker’s subframe, the browser explicitly grants the subframe read access to this temporary file (via GrantFileAccessFromDropData), and the file is passed to the attacker’s JavaScript via event.dataTransfer.files.

Suggested Attacker Steps

Note: These are potential steps as this was discovered by an LLM agent without a live execution environment.

  1. An attacker convinces a user to save an HTML file locally and open it (running in the file:// context). This file embeds the attacker’s remote page as an iframe.
  2. The attacker’s subframe contains a draggable element with a dragstart event listener.
  3. When the user drags the element, the attacker’s JavaScript executes: event.dataTransfer.setData("DownloadURL", "text/plain:secret.txt:file:///C:/target_secret.txt");
  4. The user completes the drag by dropping the item back onto the attacker’s subframe.
  5. The browser initiates a download using the main frame’s privileged context, copying the restricted local file to a temporary directory.
  6. The browser passes the temporary file to the subframe’s drop event.
  7. The attacker reads the file contents via the standard HTML5 File API (await event.dataTransfer.files[0].text();) and exfiltrates it.

Suggested Fix

  1. Validate Metadata IPC: In RenderWidgetHostImpl::StartDragging, parse the drop_data.download_metadata URL and apply GetProcess()->FilterURL() to it, discarding the metadata if the initiating process lacks the required permissions.
  2. Fix Frame Confusion: Modify DragDownloadFile to accept the actual initiating RenderFrameHost (or its IDs) rather than hardcoding web_contents->GetPrimaryMainFrame(). Note that there is already a TODO(crbug.com/40470366) in DragDownloadFileUI::InitiateDownload acknowledging this exact requirement.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results 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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker