CVE-2026-113077
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TestMouseCaptureDelegatecontent/browser/web_contents/web_contents_view_mac_unittest.mm |
modified | |
WebContentsViewMacTestcontent/browser/web_contents/web_contents_view_mac_unittest.mm |
modified | |
TEST_Fcontent/browser/web_contents/web_contents_view_mac_unittest.mm |
modified | |
ifcontent/test/BUILD.gn |
modified |
Files Changed
content/browser/web_contents/web_contents_view_mac.mmcontent/browser/web_contents/web_contents_view_mac_unittest.mmcontent/test/BUILD.gn
Patch
From 1f563afaa47fb4cbd0b3e7a56d8001c13ac5fc12 Mon Sep 17 00:00:00 2001 From: Victor Miura <[email protected]> Date: Mon, 21 Sep 2026 16:14:35 -0700 Subject: [PATCH] [mac] Don't start web content drags during mouse capture WebContentsViewMac::StartDragging refuses to start a dragging session while a window move loop owns the held mouse button, but not while a views widget owns it via mouse capture. A tab drag only runs the move loop for its detached-window phase: dragging tabs within the strip, and continuing the drag after the dragged window attaches to another tab strip, hold mouse capture instead (so do menus). A dragging session started mid-gesture would take over the held mouse button and interfere with the gesture. Refuse to start a dragging session while CocoaMouseCapture::GetGlobalCaptureWindow() is non-nil, next to the existing move loop check. Capture is released before RunMoveLoop and re-acquired synchronously when the dragged window re-attaches, so the two checks together cover the whole gesture without overlapping. Web content drags are unaffected: their mouse events are delivered to the RenderWidgetHostView's NSView directly and never take views mouse capture. TAG=agy CONV=a8d563ec-9e59-4af7-87ea-349ca7292bd9 Bug: 553159183 Change-Id: Ia6b5560a7d50f84dd4c62b0b1976972e0dc88eef Fixed: 553159183 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8433701 Auto-Submit: Victor Miura <[email protected]> Reviewed-by: Avi Drissman <[email protected]> Commit-Queue: Avi Drissman <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702168} --- diff --git a/content/browser/web_contents/web_contents_view_mac.mm b/content/browser/web_contents/web_contents_view_mac.mm index 0950326..f6a08d0 100644 --- a/content/browser/web_contents/web_contents_view_mac.mm +++ b/content/browser/web_contents/web_contents_view_mac.mm @@ -20,6 +20,7 @@ #include "base/task/current_thread.h" #include "base/task/thread_pool.h" #include "base/threading/thread_restrictions.h" +#include "components/remote_cocoa/app_shim/mouse_capture.h" #include "components/remote_cocoa/app_shim/window_move_loop.h" #include "components/remote_cocoa/browser/ns_view_ids.h" #include "components/remote_cocoa/common/application.mojom.h" @@ -234,6 +235,13 @@ web_contents_->SystemDragEnded(source_rwh); return; } + // Likewise, a widget holding mouse capture (e.g. a tab strip during a tab + // drag, or an open menu) owns the held mouse button; refuse to start a + // dragging session that would interfere with its gesture. + if (remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow()) { + web_contents_->SystemDragEnded(source_rwh); + return; + } url::Origin source_origin = source_rfh.GetLastCommittedOrigin(); // By allowing nested tasks, the code below also allows Close(), // which would deallocate |this|. The same problem can occur while 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 c348ed8..f5a3746 100644 --- a/content/browser/web_contents/web_contents_view_mac_unittest.mm +++ b/content/browser/web_contents/web_contents_view_mac_unittest.mm @@ -15,6 +15,8 @@ #include "base/functional/callback_helpers.h" #include "base/memory/weak_ptr.h" #include "base/test/run_until.h" +#include "components/remote_cocoa/app_shim/mouse_capture.h" +#include "components/remote_cocoa/app_shim/mouse_capture_delegate.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" @@ -49,6 +51,25 @@ bool was_called_ = false; }; +// Minimal capture delegate standing in for a widget that has acquired mouse +// capture (as the bridge of a views::Widget does on SetCapture()). +class TestMouseCaptureDelegate + : public remote_cocoa::CocoaMouseCaptureDelegate { + public: + explicit TestMouseCaptureDelegate(NSWindow* window) : window_(window) {} + + TestMouseCaptureDelegate(const TestMouseCaptureDelegate&) = delete; + TestMouseCaptureDelegate& operator=(const TestMouseCaptureDelegate&) = delete; + + // remote_cocoa::CocoaMouseCaptureDelegate: + bool PostCapturedEvent(NSEvent* event) override { return false; } + void OnMouseCaptureLost() override {} + NSWindow* GetWindow() const override { return window_; } + + private: + NSWindow* __strong window_; +}; + class WebContentsViewMacTest : public RenderViewHostImplTestHarness { public: void SetUp() override { @@ -126,6 +147,48 @@ EXPECT_TRUE(fake_client().was_called()); } +// A drag must not start while a widget holds mouse capture: the held mouse +// button is driving that widget's gesture (e.g. a tab drag or an open menu). +TEST_F(WebContentsViewMacTest, StartDragging_RefusedWhileMouseCaptureHeld) { + CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init]; + TestMouseCaptureDelegate capture_delegate(window); + remote_cocoa::CocoaMouseCapture capture(&capture_delegate); + ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() != nil); + + DropData drop_data; + drop_data.text = u"test data"; + + view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy, + CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(), + blink::mojom::DragEventSourceInfo()); + + // The drag request must be rejected before it reaches the policy check. + EXPECT_FALSE(fake_client().was_called()); + [window close]; +} + +// Once mouse capture is released, drag requests must go through again. +TEST_F(WebContentsViewMacTest, StartDragging_AllowedAfterMouseCaptureReleased) { + CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init]; + { + TestMouseCaptureDelegate capture_delegate(window); + remote_cocoa::CocoaMouseCapture capture(&capture_delegate); + ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() != + nil); + } + ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() == nil); + + DropData drop_data; + drop_data.text = u"test data"; + + view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy, + CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(), + blink::mojom::DragEventSourceInfo()); + + EXPECT_TRUE(fake_client().was_called()); + [window close]; +} + TEST_F(WebContentsViewMacTest, InitiallyHiddenButPaintingNativeView) { WebContents::CreateParams params(browser_context()); params.initially_hidden_but_painting = true; diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn index 8df08ed6..b25c211 100644 --- a/content/test/BUILD.gn +++ b/content/test/BUILD.gn @@ -3576,6 +3576,7 @@ } if (is_mac) { deps += [ + "//components/remote_cocoa/app_shim", "//crypto:test_support", "//device/fido:icloud_keychain_test_support", "//sandbox/mac:seatbelt",
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 c348ed8..f5a3746 100644
--- a/content/browser/web_contents/web_contents_view_mac_unittest.mm
+++ b/content/browser/web_contents/web_contents_view_mac_unittest.mm
@@ -15,6 +15,8 @@
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/test/run_until.h"
+#include "components/remote_cocoa/app_shim/mouse_capture.h"
+#include "components/remote_cocoa/app_shim/mouse_capture_delegate.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"
@@ -49,6 +51,25 @@
bool was_called_ = false;
};
+// Minimal capture delegate standing in for a widget that has acquired mouse
+// capture (as the bridge of a views::Widget does on SetCapture()).
+class TestMouseCaptureDelegate
+ : public remote_cocoa::CocoaMouseCaptureDelegate {
+ public:
+ explicit TestMouseCaptureDelegate(NSWindow* window) : window_(window) {}
+
+ TestMouseCaptureDelegate(const TestMouseCaptureDelegate&) = delete;
+ TestMouseCaptureDelegate& operator=(const TestMouseCaptureDelegate&) = delete;
+
+ // remote_cocoa::CocoaMouseCaptureDelegate:
+ bool PostCapturedEvent(NSEvent* event) override { return false; }
+ void OnMouseCaptureLost() override {}
+ NSWindow* GetWindow() const override { return window_; }
+
+ private:
+ NSWindow* __strong window_;
+};
+
class WebContentsViewMacTest : public RenderViewHostImplTestHarness {
public:
void SetUp() override {
@@ -126,6 +147,48 @@
EXPECT_TRUE(fake_client().was_called());
}
+// A drag must not start while a widget holds mouse capture: the held mouse
+// button is driving that widget's gesture (e.g. a tab drag or an open menu).
+TEST_F(WebContentsViewMacTest, StartDragging_RefusedWhileMouseCaptureHeld) {
+ CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init];
+ TestMouseCaptureDelegate capture_delegate(window);
+ remote_cocoa::CocoaMouseCapture capture(&capture_delegate);
+ ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() != nil);
+
+ DropData drop_data;
+ drop_data.text = u"test data";
+
+ view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+ CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+ blink::mojom::DragEventSourceInfo());
+
+ // The drag request must be rejected before it reaches the policy check.
+ EXPECT_FALSE(fake_client().was_called());
+ [window close];
+}
+
+// Once mouse capture is released, drag requests must go through again.
+TEST_F(WebContentsViewMacTest, StartDragging_AllowedAfterMouseCaptureReleased) {
+ CocoaTestHelperWindow* window = [[CocoaTestHelperWindow alloc] init];
+ {
+ TestMouseCaptureDelegate capture_delegate(window);
+ remote_cocoa::CocoaMouseCapture capture(&capture_delegate);
+ ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() !=
+ nil);
+ }
+ ASSERT_TRUE(remote_cocoa::CocoaMouseCapture::GetGlobalCaptureWindow() == nil);
+
+ DropData drop_data;
+ drop_data.text = u"test data";
+
+ view()->StartDragging(*main_rfh(), drop_data, blink::kDragOperationCopy,
+ CreateValidDragImage(), gfx::Vector2d(), gfx::Rect(),
+ blink::mojom::DragEventSourceInfo());
+
+ EXPECT_TRUE(fake_client().was_called());
+ [window close];
+}
+
TEST_F(WebContentsViewMacTest, InitiallyHiddenButPaintingNativeView) {
WebContents::CreateParams params(browser_context());
params.initially_hidden_but_painting = true;
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 8df08ed6..b25c211 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -3576,6 +3576,7 @@
}
if (is_mac) {
deps += [
+ "//components/remote_cocoa/app_shim",
"//crypto:test_support",
"//device/fido:icloud_keychain_test_support",
"//sandbox/mac:seatbelt",
Original Bug Report
Tab drag hijacking on macOS via WebContentsViewMac move loop guard bypass
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, WebContentsViewMac::StartDragging relies on CocoaWindowMoveLoop::IsActive() to prevent renderer-initiated drag sessions while a user is dragging a window. However, this guard is only active during the detached window move loop (kDraggingWindow), leaving within-strip tab reordering (kDraggingTabs) and post-reattach continuation phases completely unprotected. A compromised renderer could potentially invoke LocalFrameHost::StartDragging during these phases to hijack the held mouse button into an attacker-controlled NSDraggingSession.
Affected files:
content/browser/web_contents/web_contents_view_mac.mmcomponents/remote_cocoa/app_shim/window_move_loop.mmchrome/browser/ui/views/tabs/dragging/tab_drag_controller.ccui/views/cocoa/drag_drop_client_mac.mm
Estimated timestamp from git blame: 2026-08-26
Summary
On macOS, WebContentsViewMac::StartDragging uses remote_cocoa::CocoaWindowMoveLoop::IsActive() to prevent renderer-initiated drag sessions from interfering with active user drag gestures. However, CocoaWindowMoveLoop::IsActive() is scoped strictly to the lifetime of CocoaWindowMoveLoop::Run(), which only executes during the detached window move phase (DragState::kDraggingWindow) of a tab drag. Other phases where the left mouse button is held down—namely within-strip tab reordering (DragState::kDraggingTabs) and post-reattach drag continuation (DragState::kWaitingToDragTabs -> DragState::kDraggingTabs)—run without a window move loop. Consequently, a compromised renderer can trigger LocalFrameHost::StartDragging to start a native NSDraggingSession that latches onto the physically held mouse button.
Root Cause Analysis
-
Guard Scope (
components/remote_cocoa/app_shim/window_move_loop.mm:98-127):CocoaWindowMoveLoop::IsActive()checksg_move_loop_active, which is set totrueexclusively viabase::AutoResetinsideCocoaWindowMoveLoop::Run():bool CocoaWindowMoveLoop::Run() { CHECK(!g_move_loop_active); base::AutoReset<bool> active_resetter(&g_move_loop_active, true); -
Consumer Check (
content/browser/web_contents/web_contents_view_mac.mm:214-223):WebContentsViewMac::StartDraggingchecksIsActive():if (remote_cocoa::CocoaWindowMoveLoop::IsActive()) { web_contents_->SystemDragEnded(source_rwh); return; }If
IsActive()returnsfalseand no prior web drag is active on this WebContents (drag_source_start_rwh_is null), the call proceeds directly toin_process_ns_view_bridge_->StartDrag(...). -
Unprotected Tab Drag Phases (
chrome/browser/ui/views/tabs/dragging/tab_drag_controller.cc):- Within-strip dragging (
DragState::kDraggingTabs): Initiated at line 768 (StartDraggingTabsSession(true, point_in_screen)). No nested run loop is entered. Mouse capture on macOS (AttachImpl, line 1537) usesCocoaMouseCapture(components/remote_cocoa/app_shim/mouse_capture.mm:114-118), which registers passive local and globalNSEventmonitors. - Post-reattach continuation: When a detached window is dragged back over a tab strip,
DragBrowserToNewTabStrip(lines 1100-1132) callsbrowser_widget->EndMoveLoop(). WhenRunMoveLoop()returns (lines 1904-1917),g_move_loop_activeresets tofalse, but the tab drag transitions back tokDraggingTabsand continues with the physical button still held.
- Within-strip dragging (
-
Renderer Initiation and Lack of Validation:
RenderFrameHostImpl::StartDragging(content/browser/renderer_host/render_frame_host_impl.cc:11328-11355) only verifies that the document is active (IsInactiveAndDisallowActivation), omitting visibility or gesture token checks.RenderWidgetHostImpl::StartDragging(content/browser/renderer_host/render_widget_host_impl.cc:3037-3043) contains an explicit carve-out permittingjavascript:URLs.WebContentsViewCocoa(content/app_shim_remote_cocoa/web_contents_view_cocoa.mm:247-318) synthesizes anNSEventTypeLeftMouseDraggedevent frommouseLocationOutsideOfEventStreamand calls[self beginDraggingSessionWithItems:draggingItems event:dragEvent source:self]. AppKit starts an OS-level dragging session that hooks the physically held button and diverts mouse events away from the tab strip’s event monitors.
Potential Impact
A compromised renderer process can hijack the user’s ongoing tab drag gesture. When the user releases the mouse button:
- Bookmarklet Planting: If dropped over the bookmarks bar (directly adjacent to the tab strip), the un-filtered
javascript:URL creates a bookmarklet that can execute attacker code in any origin when later clicked (UXSS). - Navigation / Cross-Origin Injection: If dropped on the tab strip or another WebContents, it can trigger arbitrary URL navigation or inject attacker
DataTransferpayloads.
Suggested Reproduction Steps (Potential)
(Note: These are suggested/potential steps based on source code analysis; our tooling agent does not currently have the capability to execute code in a live macOS environment.)
- From a compromised renderer (or test harness calling
LocalFrameHost::StartDragging), prepare a payload containing ajavascript:URL or target URL. - In the browser UI, initiate an ordinary tab reordering drag within the tab strip and hold the left mouse button.
- While the button is physically held down in
DragState::kDraggingTabs, dispatch theLocalFrameHost::StartDraggingIPC from the renderer. - Observe whether
WebContentsViewMac::StartDraggingallows the request (sinceCocoaWindowMoveLoop::IsActive()isfalse) and starts anNSDraggingSessionviabeginDraggingSessionWithItems:event:source:. - Release the mouse button over the bookmarks bar or tab strip to observe whether the dropped attacker payload is processed.
Suggested Fix
Rather than relying on CocoaWindowMoveLoop::IsActive(), guard drag initiation against any active tab dragging session across all drag states:
- Check a gesture-scoped flag (e.g., query
TabDragController::IsActive()or a delegate interface on the browser window) inWebContentsViewMac::StartDraggingandDragDropClientMac::StartDragAndDrop. - Alternatively, require valid user-activation tokens or verify mouse button ownership before initiating an OS drag session via
LocalFrameHost::StartDraggingin the browser process.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
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.