CVE-2026-113218
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/web_contents/web_contents_impl.cc |
modified | |
ifcontent/browser/web_contents/web_contents_view_mac.mm |
modified | |
ifcontent/browser/web_contents/web_drag_dest_mac.mm |
modified | |
TEST_Fcontent/browser/web_contents/web_contents_view_mac_unittest.mm |
modified | |
ifcontent/test/test_web_contents.cc |
modified |
Files Changed
content/browser/web_contents/web_contents_view_mac.mmcontent/browser/web_contents/web_contents_view_mac_unittest.mmcontent/test/test_web_contents.cccontent/browser/web_contents/web_drag_dest_mac.mmcontent/browser/web_contents/web_contents_impl.cccontent/browser/web_contents/web_drag_dest_mac.h
Patch
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(
Regression Test / PoC
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(
Original Bug Report
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.mmcontent/browser/web_contents/web_contents_view_mac.hcontent/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
- An attacker-controlled main frame (
evil.com, SiteInstanceGroup X) embeds a cross-origin victim OOPIF (victim.com, SiteInstanceGroup Y). - The user begins dragging selected content inside the
victim.comsubframe, triggering the firstWebContentsViewMac::StartDraggingcall, which initializes_dragSecurityInfo.site_instance_group_id_toY. - While the drag is active, the compromised attacker renderer detaches the victim subframe (sending
FrameHost::Detach). This destroys the victim’sRenderWidgetHostImpland invalidatesdrag_source_start_rwh_in the browser process. - The compromised attacker renderer immediately sends a programmatic
LocalFrameHost::StartDraggingIPC for its own frame. - In
WebContentsViewMac::StartDragging, the reentrancy guard evaluates tofalsebecausedrag_source_start_rwh_is null. The execution continues, synchronously overwriting_dragSecurityInfo.site_instance_group_id_toX(attacker’s group). - The user releases the mouse (drops the payload) over the attacker-controlled frame area.
IsValidDragTarget(attacker_rwh)returnstruebecause the stored ID was overwritten toX. 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.