CVE-2026-113282
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/remote_cocoa/app_shim/bridged_content_view.mm |
modified | |
ifcomponents/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm |
modified | |
ifcontent/app_shim_remote_cocoa/web_contents_view_cocoa.mm |
modified | |
ifui/views/cocoa/drag_drop_client_mac.mm |
modified |
Files Changed
ui/base/cocoa/drag_permission.mmcontent/app_shim_remote_cocoa/web_contents_view_cocoa.mmui/base/BUILD.gnui/views/cocoa/drag_drop_client_mac_unittest.mmui/views/cocoa/drag_drop_client_mac.mmui/base/cocoa/drag_permission.hcontent/app/mac_init.mmcomponents/remote_cocoa/app_shim/native_widget_ns_window_bridge.mmcomponents/remote_cocoa/app_shim/bridged_content_view.mm
Patch
From efb4e25d7536583cbcebc60f99f08d8ada6304ac Mon Sep 17 00:00:00 2001
From: Avi Drissman <avi@chromium.org>
Date: Thu, 24 Sep 2026 11:47:51 -0700
Subject: [PATCH] Guard against bad drags on the Mac
There are several scenarios in which an initiated drag can go bad.
Centralize the decision-making as to whether a drag can be initiated.
Bad drag #1:
An untrustworthy renderer could disrupt a drag in progress by starting a
second one. Starting with macOS 15, AppKit checks for multiple
concurrent drags and prohibits them, but there is no such check in
earlier versions.
Also set the key in preferences to ensure that AppKit prohibits
concurrent drags.
Bad drag #2:
An untrustworthy renderer could disrupt a menu (context or popup) in
progress by starting a drag. This is not accounted for by AppKit.
Fixed: 553163770
Fixed: 553163820
Fixed: 553160293
Change-Id: I66b15a2cf220548cda92c1d42230131a6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8452662
Commit-Queue: Bryan Oltman <bryanoltman@google.com>
Reviewed-by: Bryan Oltman <bryanoltman@google.com>
Commit-Queue: Avi Drissman <avi@chromium.org>
Auto-Submit: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1704468}
---
diff --git a/components/remote_cocoa/app_shim/bridged_content_view.mm b/components/remote_cocoa/app_shim/bridged_content_view.mm
index d8f447aa3..58c8f6d 100644
--- a/components/remote_cocoa/app_shim/bridged_content_view.mm
+++ b/components/remote_cocoa/app_shim/bridged_content_view.mm
@@ -16,6 +16,7 @@
#include "components/remote_cocoa/app_shim/native_widget_ns_window_host_helper.h"
#include "components/remote_cocoa/common/native_widget_ns_window_host.mojom.h"
#import "ui/base/cocoa/appkit_utils.h"
+#include "ui/base/cocoa/drag_permission.h"
#include "ui/base/cocoa/find_pasteboard.h"
#include "ui/base/cocoa/tracking_area.h"
#include "ui/base/dragdrop/drag_drop_types.h"
@@ -1704,11 +1705,18 @@
}
- (void)draggingSession:(NSDraggingSession*)session
+ willBeginAtPoint:(NSPoint)screenPoint {
+ ui::SetDragSessionInProgress(true);
+}
+
+- (void)draggingSession:(NSDraggingSession*)session
endedAtPoint:(NSPoint)screenPoint
operation:(NSDragOperation)operation {
+ ui::SetDragSessionInProgress(false);
remote_cocoa::DragDropClient* client = [self dragDropClient];
- if (client)
+ if (client) {
client->EndDrag();
+ }
}
// NSAccessibility formal protocol implementation:
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
index 7581294..1c4d2363 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
@@ -48,6 +48,7 @@
#include "ui/base/cocoa/animation_utils.h"
#import "ui/base/cocoa/constrained_window/constrained_window_animation.h"
#include "ui/base/cocoa/cursor_utils.h"
+#include "ui/base/cocoa/drag_permission.h"
#include "ui/base/cocoa/remote_accessibility_api.h"
#import "ui/base/cocoa/window_size_constants.h"
#include "ui/base/emoji/emoji_panel_helper.h"
@@ -1406,6 +1407,10 @@
DLOG(WARNING) << "BeginFileDrag failed: window or bridged_view is null";
return;
}
+ if (!ui::IsDragSessionInitiationAllowed()) {
+ DLOG(WARNING) << "BeginFileDrag failed: drag session not allowed";
+ return;
+ }
NSURL* file_url = base::apple::FilePathToNSURL(file_drag_data->file_path);
if (!file_url) {
diff --git a/content/app/mac_init.mm b/content/app/mac_init.mm
index dc4067c6..25452ebd 100644
--- a/content/app/mac_init.mm
+++ b/content/app/mac_init.mm
@@ -28,6 +28,19 @@
// Chrome is unusable for a long period after returning from sleep.
// https://crbug.com/41406192.
@"NSAppSleepDisabled" : @YES,
+
+ // Prior to macOS 15, AppKit assumed that apps would not be so silly as to
+ // attempt to start a concurrent drag session while one was already in
+ // progress. Starting in macOS 15, AppKit guards against it, but it has a
+ // BoolAppConfig to allow it, presumably to allow for a case where this
+ // would be needed for compatibility. Given that starting a second drag
+ // while one is in progress could be used to mount an attack on the user,
+ // explicitly set the config. See -[NSCoreDragManager
+ // beginDraggingSessionWithItems:fromWindow:withClipRect:event:source:] for
+ // where the config is accessed, and see https://crbug.com/553160293,
+ // https://crbug.com/553163770, and https://crbug.com/553163820 for the
+ // rationale.
+ @"NSAllowMultipleDrags" : @NO,
}];
// Disable NSAutoFillHeuristicController on macOS 26.0 and 26.1. On those OS
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 94da754..60bb2a2 100644
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
@@ -20,6 +20,7 @@
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/clipboard_util_mac.h"
#include "ui/base/clipboard/custom_data_helper.h"
+#include "ui/base/cocoa/drag_permission.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/events/event_utils.h"
#include "ui/events/platform_event.h"
@@ -241,8 +242,9 @@
image:(NSImage*)image
offset:(NSPoint)offset
isPrivileged:(BOOL)isPrivileged {
- if (!_host)
+ if (!_host || !ui::IsDragSessionInitiationAllowed()) {
return;
+ }
NSPoint mouseLocation = [self.window mouseLocationOutsideOfEventStream];
NSEvent* dragEvent = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDragged
@@ -325,10 +327,16 @@
return _dragOperation;
}
+- (void)draggingSession:(NSDraggingSession*)session
+ willBeginAtPoint:(NSPoint)screenPoint {
+ ui::SetDragSessionInProgress(true);
+}
+
// Called when a drag initiated in our view ends.
- (void)draggingSession:(NSDraggingSession*)session
endedAtPoint:(NSPoint)screenPoint
operation:(NSDragOperation)operation {
+ ui::SetDragSessionInProgress(false);
if (!_host) {
return;
}
diff --git a/ui/base/cocoa/drag_permission.h b/ui/base/cocoa/drag_permission.h
new file mode 100644
index 0000000..841e1d21
--- /dev/null
+++ b/ui/base/cocoa/drag_permission.h
@@ -0,0 +1,37 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_BASE_COCOA_DRAG_PERMISSION_H_
+#define UI_BASE_COCOA_DRAG_PERMISSION_H_
+
+#include "base/component_export.h"
+
+// There are several times when it would be bad to allow a drag to be initiated.
+// These are often not accounted for by AppKit, which assumes a reasonable and
+// non-adversarial usage of its APIs. This file handles coordination of defense
+// against those attacks.
+//
+// This class is meant to prevent the initiation of a drag session while:
+// - a drag session is already in progress
+// - a menu tracking session is already in progress
+//
+// macOS 15+ does prevent the first, but it does not prevent the second. Given
+// that these are security concerns, take this into our own hands.
+//
+// See https://crbug.com/553160293, https://crbug.com/553163770, and
+// https://crbug.com/553163820 for the concerns with concurrent drag sessions.
+
+namespace ui {
+
+// Returns whether a drag session may be initiated.
+COMPONENT_EXPORT(UI_BASE)
+bool IsDragSessionInitiationAllowed();
+
+// Sets the state of whether a drag session is in progress.
+COMPONENT_EXPORT(UI_BASE)
+void SetDragSessionInProgress(bool in_progress);
+
+} // namespace ui
+
+#endif // UI_BASE_COCOA_DRAG_PERMISSION_H_
diff --git a/ui/base/cocoa/drag_permission.mm b/ui/base/cocoa/drag_permission.mm
new file mode 100644
index 0000000..7008f90
--- /dev/null
+++ b/ui/base/cocoa/drag_permission.mm
@@ -0,0 +1,33 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/base/cocoa/drag_permission.h"
+
+#import <AppKit/AppKit.h>
+
+#include "base/check_op.h"
+
+namespace ui {
+
+namespace {
+
+// There is SPI to inquire as to the existence of a current drag session
+// ([_NSDragManager isDragging]). However, usage of SPI is avoided if it is easy
+// to do so, and it is easy in this case.
+bool gDragSessionIsInProgress;
+
+} // namespace
+
+bool IsDragSessionInitiationAllowed() {
+ CHECK_EQ(NSRunLoop.currentRunLoop, NSRunLoop.mainRunLoop);
+ return !gDragSessionIsInProgress &&
+ NSRunLoop.currentRunLoop.currentMode != NSEventTrackingRunLoopMode;
+}
+
+void SetDragSessionInProgress(bool in_progress) {
+ CHECK_NE(gDragSessionIsInProgress, in_progress);
+ gDragSessionIsInProgress = in_progress;
+}
+
+} // namespace ui
diff --git a/ui/views/cocoa/drag_drop_client_mac.mm b/ui/views/cocoa/drag_drop_client_mac.mm
index 2a583d1..22934a2 100644
--- a/ui/views/cocoa/drag_drop_client_mac.mm
+++ b/ui/views/cocoa/drag_drop_client_mac.mm
@@ -10,6 +10,7 @@
#import "components/remote_cocoa/app_shim/bridged_content_view.h"
#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "components/remote_cocoa/app_shim/window_move_loop.h"
+#include "ui/base/cocoa/drag_permission.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#import "ui/base/dragdrop/os_exchange_data_provider_mac.h"
@@ -38,6 +39,10 @@
return;
}
+ if (!ui::IsDragSessionInitiationAllowed()) {
+ return;
+ }
+
exchange_data_ = std::move(data);
source_operation_ = operation;
is_drag_source_ = true;
@@ -97,9 +102,17 @@
}
}
- [bridge_->ns_view() beginDraggingSessionWithItems:drag_items
- event:event
- source:bridge_->ns_view()];
+ NSDraggingSession* session =
+ [bridge_->ns_view() beginDraggingSessionWithItems:drag_items
+ event:event
+ source:bridge_->ns_view()];
+ if (!session) {
+ // The return value from -beginDraggingSessionWithItems:event:source: is
+ // marked as nonnull, but in the edge case where there already is an ongoing
+ // dragging session, nil is returned. (The issue with the SDK is filed as
+ // FB24907244.) In that case, bail.
+ return;
+ }
// Since Drag and drop is asynchronous on the Mac, spin a nested run loop for
// consistency with other platforms.
@@ -107,12 +120,12 @@
quit_closure_ = run_loop.QuitClosure();
run_loop.Run();
- // As of MacOS 26, dragging with a trackpad results in a leftover mouse-up
+ // As of macOS 26, dragging with a trackpad results in a leftover mouse-up
// event remaining in the queue when the drag session is terminated. This
// ensures any leftover message is pumped to avoid handling the "release"
// action as a click.
[NSApp nextEventMatchingMask:NSEventMaskLeftMouseUp
- untilDate:[NSDate distantPast]
+ untilDate:NSDate.distantPast
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
}
diff --git a/ui/views/cocoa/drag_drop_client_mac_unittest.mm b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
index 15de877..5f4f25d 100644
--- a/ui/views/cocoa/drag_drop_client_mac_unittest.mm
+++ b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
@@ -40,7 +40,12 @@
source:(id<NSDraggingSource>)
source {
++g_begin_dragging_session_call_count;
- return nil;
+
+ // Return a fake dragging session to pretend that the dragging initiation was
+ // successful. The only thing done to the return object is a nil check, so a
+ // raw NSObject will do, and if that ever changes, a blank object will explode
+ // immediately, alerting to the issue.
+ return (NSDraggingSession*)[[NSObject alloc] init];
}
@end
diff --git a/ui/base/BUILD.gn b/ui/base/BUILD.gn
index 39c46572..78a13bb 100644
--- a/ui/base/BUILD.gn
+++ b/ui/base/BUILD.gn
@@ -319,6 +319,8 @@
"cocoa/default_command_dispatcher_delegate.mm",
"cocoa/defaults_utils.h",
"cocoa/defaults_utils.mm",
+ "cocoa/drag_permission.h",
+ "cocoa/drag_permission.mm",
"cocoa/find_pasteboard.h",
"cocoa/find_pasteboard.mm",
"cocoa/menu_utils.h",
Regression Test / PoC
diff --git a/ui/views/cocoa/drag_drop_client_mac_unittest.mm b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
index 15de877..5f4f25d 100644
--- a/ui/views/cocoa/drag_drop_client_mac_unittest.mm
+++ b/ui/views/cocoa/drag_drop_client_mac_unittest.mm
@@ -40,7 +40,12 @@
source:(id<NSDraggingSource>)
source {
++g_begin_dragging_session_call_count;
- return nil;
+
+ // Return a fake dragging session to pretend that the dragging initiation was
+ // successful. The only thing done to the return object is a nil check, so a
+ // raw NSObject will do, and if that ever changes, a blank object will explode
+ // immediately, alerting to the issue.
+ return (NSDraggingSession*)[[NSObject alloc] init];
}
@end
Original Bug Report
Missing global drag guard in DragDropClientMac allows drag hijacking on macOS
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, DragDropClientMac::StartDragAndDrop spins a nested message loop during Views drag operations without setting or checking a process-global drag-in-progress guard. While a user performs a drag gesture in the browser UI, a compromised renderer can potentially invoke LocalFrameHost::StartDragging from a secondary window. This could allow an attacker to initiate a competing dragging session and substitute malicious data into the in-flight drag gesture or corrupt browser drag state.
Affected files:
ui/views/cocoa/drag_drop_client_mac.mmcontent/browser/web_contents/web_contents_view_mac.mm
Estimated timestamp from git blame: 2019-02-26
Summary
In ui/views/cocoa/drag_drop_client_mac.mm, views::DragDropClientMac::StartDragAndDrop initiates a native macOS dragging session and spins a nested task-pumping base::RunLoop(kNestableTasksAllowed) without maintaining or checking a process-global drag-in-progress flag. Sibling platforms (Windows and Ozone) maintain a global g_is_dragging guard to prevent a secondary window or renderer from dispatching a competing drag session while a nested loop is pumping tasks. On macOS, this check is absent, creating a potential port-gap vulnerability where a compromised renderer can trigger concurrent drag sessions and potentially hijack user-initiated drag gestures.
Root Cause Analysis
When a user initiates a Views drag operation (such as dragging a bookmark from BookmarkBarView, a download item, or an item from a tab group), the execution reaches DragDropClientMac::StartDragAndDrop (ui/views/cocoa/drag_drop_client_mac.mm:31-118):
void DragDropClientMac::StartDragAndDrop(
std::unique_ptr<ui::OSExchangeData> data,
int operation,
ui::mojom::DragEventSource source) {
if (remote_cocoa::CocoaWindowMoveLoop::IsActive()) {
return;
}
exchange_data_ = std::move(data);
source_operation_ = operation;
is_drag_source_ = true;
...
[bridge_->ns_view() beginDraggingSessionWithItems:drag_items
event:event
source:bridge_->ns_view()];
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
quit_closure_ = run_loop.QuitClosure();
run_loop.Run();
...
}
Because Widget::RunDragDropLoop instantiates base::CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop (ui/views/widget/widget.cc:1473), application tasks and Mojo IPCs from renderer processes continue to be pumped during the nested run_loop.Run() while the user physically holds down the mouse button.
On Windows and Linux/Ozone, this reentrancy hazard across multiple windows is prevented by a process-global guard:
ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc:77:CHECK(!g_is_dragging);ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc:162:if (g_is_dragging) return DragOperation::kNone;
On macOS, neither DragDropClientMac nor WebContentsViewMac consults a shared global drag flag:
DragDropClientMaconly checksremote_cocoa::CocoaWindowMoveLoop::IsActive(), but sets no global flag.WebContentsViewMac::StartDragging(content/browser/web_contents/web_contents_view_mac.mm:215) only checks its own per-instancedrag_source_start_rwh_andCocoaWindowMoveLoop::IsActive(). If a drag request arrives from a differentWebContentsinstance (such as a popup window), the check passes and invokesWebContentsViewCocoa startDragWithDropData:, which begins a second AppKit dragging session ([self beginDraggingSessionWithItems:event:source:]).
Potential Attack Steps
(Note: These are suggested/potential steps derived from static code analysis; our tooling agent does not currently have the environment to run live macOS code.)
- An attacker with a compromised renderer opens a secondary popup window ($W_2$) using
window.open(). - The user initiates a drag gesture on a Views component (e.g., dragging a bookmark on the bookmark bar in window $W_1$). The browser process UI thread enters
DragDropClientMac::StartDragAndDropand runs a nested message loop while the user holds the mouse button. - The compromised renderer sends a
blink::mojom::LocalFrameHost::StartDraggingMojo IPC on $W_2$’s frame with attacker-controlled drag data (e.g., ajavascript:...bookmarklet URL, which is explicitly allowed throughRenderWidgetHostImpl::StartDraggingvia lines 3037-3043). - The IPC is dispatched inside the nested run loop on the browser UI thread.
WebContentsViewMac::StartDraggingdoes not detect the active Views drag and proceeds to callWebContentsViewCocoa startDragWithDropData:. WebContentsViewCocoasynthesizes anNSEventTypeLeftMouseDraggedevent at the live cursor position ([self.window mouseLocationOutsideOfEventStream]) and initiates a concurrent AppKit dragging session.- When the user releases the mouse button over the Bookmark Bar or another drop target, the system processes the attacker’s payload instead of or in interference with the original drag item.
Security Impact
- Gesture Hijacking (CWE-1021): The attacker’s data (such as a
javascript:URL) can be dropped into privileged UI locations (e.g. Bookmark Bar) under the guise of the user’s legitimate drag gesture. - Drag State Corruption: Uncoordinated concurrent sessions in AppKit can leave browser-process drag tracking variables (
_dragSource,drag_source_start_rwh_,exchange_data_) in an inconsistent state.
Suggested Fix
Introduce a process-global drag-in-progress guard for macOS (mirroring DesktopDragDropClientOzone / DesktopDragDropClientWin or CocoaWindowMoveLoop):
- Define a global flag (e.g.,
remote_cocoa::IsDragDropInProgress()) or integrate a common global drag scoper. - In
DragDropClientMac::StartDragAndDrop, reject or skip starting a new drag session if a drag (or move loop) is already active, and set the global flag for the duration of the nested loop. - In
WebContentsViewMac::StartDragging, check the global drag state and immediately reject re-entrant drag attempts across all windows while another drag session is active.
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.