Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DataTransfer
DescriptionUse after free in DataTransfer
ComponentDataTransfer
Sub-componentBlink>DataTransfer, Internals>Sandbox>SiteIsolation
Bug ClassUAF
Tracker518003952
Fix commitcd38ff4dfb04 (chromium/src) +62/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
content/browser/web_contents/web_contents_impl.cc
modified
if
content/browser/web_contents/web_contents_view_mac.mm
modified
if
content/browser/web_contents/web_drag_dest_mac.mm
modified
TEST_F
content/browser/web_contents/web_contents_view_mac_unittest.mm
modified
if
content/test/test_web_contents.cc
modified

Files Changed

  • content/browser/web_contents/web_contents_view_mac.mm
  • content/browser/web_contents/web_contents_view_mac_unittest.mm
  • content/test/test_web_contents.cc
  • content/browser/web_contents/web_drag_dest_mac.mm
  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/web_contents/web_drag_dest_mac.h
From cd38ff4dfb041f69d7a9a502dbc1e1c88e294773 Mon Sep 17 00:00:00 2001
From: Jocelyn Tran <jocelyntran@google.com>
Date: Wed, 16 Sep 2026 16:40:10 -0700
Subject: [PATCH] Improve drag and drop state tracking on Mac

Previously, the browser prevented overlapping drag operations by
checking if drag_source_start_rwh_ was set. However, if the source frame
was destroyed while the macOS dragging session was still active, this
pointer would become null. This could lead to the browser incorrectly
processing subsequent StartDragging IPCs during an ongoing drag session.

To address this, this CL:
- Updates the reentrancy check in StartDragging to use
[drag_dest_ didInitiateDrag], which correctly tracks the lifecycle of
the actual macOS dragging session. This matches the Aura and Android
implementations which already use the same check.

Bug: 518003952
Change-Id: I85d47a02bac89aeab9ce63b53d807e18c65c5c14
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8376785
Commit-Queue: Jocelyn Tran <jocelyntran@google.com>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699342}
---
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 3c4f7b1..ba69b6a3 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -4856,6 +4856,10 @@
     const gfx::PointF& point,
     base::OnceCallback<void(base::WeakPtr<RenderWidgetHostViewBase>,
                             std::optional<gfx::PointF>)> callback) {
+  if (!root_view) {
+    std::move(callback).Run(nullptr, std::nullopt);
+    return;
+  }
   GetInputEventRouter()->GetRenderWidgetHostAtPointAsynchronously(
       root_view, point, base::BindOnce(&RunCallback, std::move(callback)));
 }
diff --git a/content/browser/web_contents/web_contents_view_mac.mm b/content/browser/web_contents/web_contents_view_mac.mm
index baf225df..0950326 100644
--- a/content/browser/web_contents/web_contents_view_mac.mm
+++ b/content/browser/web_contents/web_contents_view_mac.mm
@@ -223,8 +223,9 @@
     const blink::mojom::DragEventSourceInfo& event_info) {
   RenderWidgetHostImpl* source_rwh =
       static_cast<RenderWidgetHostImpl*>(source_rfh.GetRenderWidgetHost());
-  // Disallow reentrant drag which could be an attempt to exploit drag state.
-  if (drag_source_start_rwh_) {
+  // Disallow reentrant drags. This uses the destination's state because
+  // the source frame might be destroyed during an active drag session.
+  if ([drag_dest_ didInitiateDrag] || drag_source_start_rwh_) {
     return;
   }
   // A window move loop already owns the held mouse button; refuse to start a
@@ -693,7 +694,7 @@
                                         const gfx::PointF& screen_point) {
   // Validate internal members are non-null as this method can be called
   // asynchronously.
-  if (!web_contents_ || !drag_source_start_rwh_) {
+  if (!web_contents_) {
     return;
   }
 
@@ -703,17 +704,19 @@
   // non-root RenderWidgetHosts they need to be transformed.
   gfx::PointF transformed_point = local_point;
   gfx::PointF transformed_screen_point = screen_point;
-  if (web_contents_->GetRenderWidgetHostView()) {
+  if (drag_source_start_rwh_ && web_contents_->GetRenderWidgetHostView()) {
     content::RenderWidgetHostViewBase* contentsViewBase =
         static_cast<content::RenderWidgetHostViewBase*>(
             web_contents_->GetRenderWidgetHostView());
     content::RenderWidgetHostViewBase* dragStartViewBase =
         static_cast<content::RenderWidgetHostViewBase*>(
             drag_source_start_rwh_->GetView());
-    contentsViewBase->TransformPointToCoordSpaceForView(
-        local_point, dragStartViewBase, &transformed_point);
-    contentsViewBase->TransformPointToCoordSpaceForView(
-        screen_point, dragStartViewBase, &transformed_screen_point);
+    if (dragStartViewBase) {
+      contentsViewBase->TransformPointToCoordSpaceForView(
+          local_point, dragStartViewBase, &transformed_point);
+      contentsViewBase->TransformPointToCoordSpaceForView(
+          screen_point, dragStartViewBase, &transformed_screen_point);
+    }
   }
 
   web_contents_->DragSourceEndedAt(
diff --git a/content/browser/web_contents/web_drag_dest_mac.h b/content/browser/web_contents/web_drag_dest_mac.h
index 0bed1c00..175fbf4 100644
--- a/content/browser/web_contents/web_drag_dest_mac.h
+++ b/content/browser/web_contents/web_drag_dest_mac.h
@@ -102,6 +102,11 @@
 - (void)initiateDragWithRenderWidgetHost:(content::RenderWidgetHostImpl*)rwhi
                                 dropData:(const content::DropData&)dropData;
 
+// Returns whether the owning WebContents has an in-progress drag (i.e.
+// `initiateDragWithRenderWidgetHost:dropData:` has been called without a
+// matching `endDrag:`).
+- (bool)didInitiateDrag;
+
 // Called to indicate that, if the owning WebContents has initiated a drag, that
 // drag has ended. The closure will be called synchronously if the "dragend"
 // event can be fired, or asynchronously if a "drop" event is still pending
diff --git a/content/browser/web_contents/web_drag_dest_mac.mm b/content/browser/web_contents/web_drag_dest_mac.mm
index a62d340..9ff678c 100644
--- a/content/browser/web_contents/web_drag_dest_mac.mm
+++ b/content/browser/web_contents/web_drag_dest_mac.mm
@@ -505,10 +505,12 @@
       content::RenderWidgetHostViewBase* currentDragView =
           static_cast<content::RenderWidgetHostViewBase*>(
               _currentRWHForDrag->GetView());
-      rootView->TransformPointToCoordSpaceForView(
-          transformedLeavePoint, currentDragView, &transformedLeavePoint);
-      rootView->TransformPointToCoordSpaceForView(
-          transformedScreenPoint, currentDragView, &transformedScreenPoint);
+      if (rootView && currentDragView) {
+        rootView->TransformPointToCoordSpaceForView(
+            transformedLeavePoint, currentDragView, &transformedLeavePoint);
+        rootView->TransformPointToCoordSpaceForView(
+            transformedScreenPoint, currentDragView, &transformedScreenPoint);
+      }
       _currentRWHForDrag->DragTargetDragLeave(transformedLeavePoint,
                                               transformedScreenPoint);
     }
@@ -736,6 +738,10 @@
   _dragSecurityInfo.OnDragInitiated(rwhi, dropData);
 }
 
+- (bool)didInitiateDrag {
+  return _dragSecurityInfo.did_initiate();
+}
+
 - (void)endDrag:(base::OnceClosure)closure {
   _dragSecurityInfo.OnDragEnded();
 
diff --git a/content/browser/web_contents/web_contents_view_mac_unittest.mm b/content/browser/web_contents/web_contents_view_mac_unittest.mm
index 2786f304..c348ed8 100644
--- a/content/browser/web_contents/web_contents_view_mac_unittest.mm
+++ b/content/browser/web_contents/web_contents_view_mac_unittest.mm
@@ -12,10 +12,12 @@
 #include "base/containers/to_vector.h"
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
+#include "base/functional/callback_helpers.h"
 #include "base/memory/weak_ptr.h"
 #include "base/test/run_until.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
+#import "content/browser/web_contents/web_drag_dest_mac.h"
 #include "content/public/browser/web_contents_view_delegate.h"
 #include "content/public/common/drop_data.h"
 #include "content/public/test/browser_test_utils.h"
@@ -99,6 +101,31 @@
   EXPECT_TRUE(fake_client().was_called());
 }
 
+TEST_F(WebContentsViewMacTest, StartDragging_RejectedWhileDragInProgress) {
+  DropData drop_data;
+  drop_data.text = u"test data";
+
+  // Record that the drag destination has an in-progress drag, as would happen
+  // for a previously started drag whose dragging session is still active.
+  [view()->drag_dest() initiateDragWithRenderWidgetHost:GetRenderWidgetHost()
+                                               dropData:drop_data];
+
+  // A second StartDragging must be rejected before consulting the policy
+  // delegate or otherwise touching drag state.
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+  EXPECT_FALSE(fake_client().was_called());
+
+  // Once the in-progress drag has ended, StartDragging proceeds as normal.
+  [view()->drag_dest() endDrag:base::DoNothing()];
+
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+  EXPECT_TRUE(fake_client().was_called());
+}
+
 TEST_F(WebContentsViewMacTest, InitiallyHiddenButPaintingNativeView) {
   WebContents::CreateParams params(browser_context());
   params.initially_hidden_but_painting = true;
diff --git a/content/test/test_web_contents.cc b/content/test/test_web_contents.cc
index 2110366..04d5b5b 100644
--- a/content/test/test_web_contents.cc
+++ b/content/test/test_web_contents.cc
@@ -671,6 +671,11 @@
     const gfx::PointF& point,
     base::OnceCallback<void(base::WeakPtr<RenderWidgetHostViewBase>,
                             std::optional<gfx::PointF>)> callback) {
+  if (!root_view) {
+    std::move(callback).Run(nullptr, std::nullopt);
+    return;
+  }
+
   // If defer flag is disabled, call base implementation synchronously.
   if (!defer_get_render_widget_host_at_point_) {
     WebContentsImpl::GetRenderWidgetHostAtPointAsynchronously(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/web_contents/web_contents_view_mac_unittest.mm b/content/browser/web_contents/web_contents_view_mac_unittest.mm
index 2786f304..c348ed8 100644
--- a/content/browser/web_contents/web_contents_view_mac_unittest.mm
+++ b/content/browser/web_contents/web_contents_view_mac_unittest.mm
@@ -12,10 +12,12 @@
 #include "base/containers/to_vector.h"
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
+#include "base/functional/callback_helpers.h"
 #include "base/memory/weak_ptr.h"
 #include "base/test/run_until.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
+#import "content/browser/web_contents/web_drag_dest_mac.h"
 #include "content/public/browser/web_contents_view_delegate.h"
 #include "content/public/common/drop_data.h"
 #include "content/public/test/browser_test_utils.h"
@@ -99,6 +101,31 @@
   EXPECT_TRUE(fake_client().was_called());
 }
 
+TEST_F(WebContentsViewMacTest, StartDragging_RejectedWhileDragInProgress) {
+  DropData drop_data;
+  drop_data.text = u"test data";
+
+  // Record that the drag destination has an in-progress drag, as would happen
+  // for a previously started drag whose dragging session is still active.
+  [view()->drag_dest() initiateDragWithRenderWidgetHost:GetRenderWidgetHost()
+                                               dropData:drop_data];
+
+  // A second StartDragging must be rejected before consulting the policy
+  // delegate or otherwise touching drag state.
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+  EXPECT_FALSE(fake_client().was_called());
+
+  // Once the in-progress drag has ended, StartDragging proceeds as normal.
+  [view()->drag_dest() endDrag:base::DoNothing()];
+
+  view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+                        CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+                        blink::mojom::DragEventSourceInfo());
+  EXPECT_TRUE(fake_client().was_called());
+}
+
 TEST_F(WebContentsViewMacTest, InitiallyHiddenButPaintingNativeView) {
   WebContents::CreateParams params(browser_context());
   params.initially_hidden_but_painting = true;
diff --git a/content/test/test_web_contents.cc b/content/test/test_web_contents.cc
index 2110366..04d5b5b 100644
--- a/content/test/test_web_contents.cc
+++ b/content/test/test_web_contents.cc
@@ -671,6 +671,11 @@
     const gfx::PointF& point,
     base::OnceCallback<void(base::WeakPtr<RenderWidgetHostViewBase>,
                             std::optional<gfx::PointF>)> callback) {
+  if (!root_view) {
+    std::move(callback).Run(nullptr, std::nullopt);
+    return;
+  }
+
   // If defer flag is disabled, call base implementation synchronously.
   if (!defer_get_render_widget_host_at_point_) {
     WebContentsImpl::GetRenderWidgetHostAtPointAsynchronously(
Loading diff…

Original Bug Report

reported by vm...@google.com

Bypass of cross-origin drag protection on macOS via RWH destruction mid-drag

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: On macOS, the drag-and-drop reentrancy guard in WebContentsViewMac::StartDragging relies on a base::WeakPtr to the source RenderWidgetHostImpl. If a compromised renderer initiates a drag and immediately detaches the source iframe, this weak pointer is invalidated and the reentrancy guard fails open. This allows the attacker to invoke a second programmatic drag operation, overwriting the browser-side WebContentsViewDragSecurityInfo and bypassing cross-origin drag protection to leak victim data.

Affected files:

  • content/browser/web_contents/web_contents_view_mac.mm
  • content/browser/web_contents/web_contents_view_mac.h
  • content/browser/web_contents/web_drag_dest_mac.mm

Estimated timestamp from git blame: 2026-04-24

Description

There is a potential vulnerability in the macOS drag-and-drop implementation where same-page cross-origin drag protection can be bypassed by a compromised renderer process.

Unlike the Aura implementation which uses a robust reentrancy guard keyed on a persistent boolean state (drag_security_info_.did_initiate()), the macOS implementation in WebContentsViewMac::StartDragging utilizes a weak pointer to the source RenderWidgetHostImpl (drag_source_start_rwh_) as its reentrancy guard:

// content/browser/web_contents/web_contents_view_mac.mm
RenderWidgetHostImpl* source_rwh = 
    static_cast<RenderWidgetHostImpl*>(source_rfh.GetRenderWidgetHost());
// Disallow reentrant drag which could be an attempt to exploit drag state.
if (drag_source_start_rwh_) {
  return;
}

If the initiating source frame is detached while the drag session is active, its RenderWidgetHostImpl is destroyed. This automatically invalidates drag_source_start_rwh_ (making it nullptr and causing operator bool() to evaluate to false), failing the guard open.

When a subsequent programmatic StartDragging IPC is sent by the compromised renderer, the reentrancy check passes. This second call invokes -[WebDragDest initiateDragWithRenderWidgetHost:dropData:], which unconditionally overwrites the browser’s drag security state (_dragSecurityInfo):

// content/browser/web_contents/web_drag_dest_mac.mm
- (void)initiateDragWithRenderWidgetHost:(content::RenderWidgetHostImpl*)rwhi
                                dropData:(const content::DropData&)dropData {
  _dragSecurityInfo.OnDragInitiated(rwhi, dropData);
}

By overwriting _dragSecurityInfo.site_instance_group_id_ with the attacker’s SiteInstanceGroupId, the browser-side target validation (IsValidDragTarget) will succeed when dropping onto the attacker frame, allowing the compromised renderer to capture the sensitive cross-origin drop payload.

Note: These are suggested/potential steps based on manual static analysis of the codebase. Our tooling agent does not currently have the capability to execute code or run functional proof-of-concept exploits.

Potential Attack Sequence

  1. An attacker-controlled main frame (evil.com, SiteInstanceGroup X) embeds a cross-origin victim OOPIF (victim.com, SiteInstanceGroup Y).
  2. The user begins dragging selected content inside the victim.com subframe, triggering the first WebContentsViewMac::StartDragging call, which initializes _dragSecurityInfo.site_instance_group_id_ to Y.
  3. While the drag is active, the compromised attacker renderer detaches the victim subframe (sending FrameHost::Detach). This destroys the victim’s RenderWidgetHostImpl and invalidates drag_source_start_rwh_ in the browser process.
  4. The compromised attacker renderer immediately sends a programmatic LocalFrameHost::StartDragging IPC for its own frame.
  5. In WebContentsViewMac::StartDragging, the reentrancy guard evaluates to false because drag_source_start_rwh_ is null. The execution continues, synchronously overwriting _dragSecurityInfo.site_instance_group_id_ to X (attacker’s group).
  6. The user releases the mouse (drops the payload) over the attacker-controlled frame area.
  7. IsValidDragTarget(attacker_rwh) returns true because the stored ID was overwritten to X. The browser delivers the original victim drag payload to the attacker’s renderer.

Impact

This bypass allows a compromised renderer to steal sensitive cross-origin drag payloads (such as selected text, HTML, or URLs) from other frames on the page, resulting in a Site Isolation bypass.

Suggested Fix

Align the macOS reentrancy guard with the Aura implementation by checking the status of _dragSecurityInfo in WebDragDest instead of relying on a weak pointer to the source widget host. Alternatively, track whether a drag is currently active on the Mac view using a persistent boolean flag that is only reset when the drag session has explicitly completed (OnDragEnded()).

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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