CVE-2026-87644
Overview
Background
- Renderer-initiated drag
- a drag-and-drop operation that a web page’s renderer process asks the browser to begin, supplying data and a starting screen location in
event_info. - `WebContentsViewAura::StartDragging`
- the browser-process (Aura) entry point that takes the renderer’s drag request and forwards it to the platform
DragDropClient::StartDragAndDrop(function name(inferred)from the regression test, not shown in the hunk header). - `aura::Env`
- the browser-side singleton that tracks browser-observed input state, exposing
is_touch_down(),IsMouseButtonDown(),GetLastPointerPoint(...), andlast_mouse_location()as trusted ground truth. - `event_info.location`
- the screen-coordinate point carried in the renderer’s drag request; because it originates in the renderer it is attacker-controllable and untrusted.
Root Cause Analysis
In the pre-patch WebContentsViewAura::StartDragging, only the touch-initiated path replaced the untrusted event_info.location with a browser-observed point via env->GetLastPointerPoint(...); for a mouse-initiated drag (ui::mojom::DragEventSource::kMouse) the code left trusted_location = event_info.location, i.e. it forwarded the renderer-supplied screen location straight through to DragDropClient::StartDragAndDrop. The only mouse-side safeguard was the later “event is within the web contents” bounds check, which still trusted a renderer-chosen point and did not confirm that a real mouse drag was actually in progress. This violated the invariant that OS-level drag/synthesized-input coordinates must come from browser-observed input, not from the renderer.
The fix adds a guard that rejects the drag (calling web_contents_->SystemDragEnded(source_rwh) then return) when the claimed source has no matching in-flight input — !env->is_touch_down() for touch or !env->IsMouseButtonDown() for mouse — and, for the mouse path, sets trusted_location = env->last_mouse_location() so the renderer value can no longer flow downstream. The change is a tightening: it adds a required-input precondition and substitutes the trusted browser point, matching the clamping already done for touch.
event_info.location for mouse-initiated drags while only touch drags were clamped to a browser-observed point; the fix requires a real mouse button to be down and overwrites the location with aura::Env::last_mouse_location(), denying the renderer control over where the drag/synthesized input is delivered.Attack Path
- Compromised renderer starts a mouse drag
A malicious or compromised renderer issues a renderer-initiated drag with
source == ui::mojom::DragEventSource::kMouseand no genuine user mouse-button press behind it. - Attacker-chosen location is supplied
The renderer sets
event_info.locationto an arbitrary screen point of its choosing rather than the real cursor position. - Unclamped location reaches the platform
Pre-patch
WebContentsViewAura::StartDraggingkeepstrusted_location = event_info.locationfor mouse and passes it toDragDropClient::StartDragAndDrop, so the renderer-controlled point drives the OS drag. - Synthesized input is redirected
On Windows the location flows to
::SendInput, which can redirect the synthesized click/drag onto an overlapping browser-privileged HWND such as a permission bubble(inferred)from the patch comment describing the analogous touch path. - Unauthorized UI interaction
The renderer thereby influences or activates browser UI the user never targeted, an incorrect-authorization (“Incorrect authorization in Views”) outcome
(inferred)as to the specific privileged control actuated.
Impact Assessment
(inferred) from the patch comment and not demonstrated by the diff. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/web_contents/web_contents_view_aura.cc |
modified | |
TestDragDropClientchrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc |
modified | |
ifchrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc |
modified | |
forchrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc |
modified | |
BindLambdaForTestingchrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc |
modified | |
PollUntilchrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc |
modified | |
Dochrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc |
modified | |
TEST_Fcontent/browser/web_contents/web_contents_view_aura_unittest.cc |
modified |
Files Changed
ui/aura/test/env_test_helper.hchrome/browser/ui/views/toolbar/BUILD.gncontent/public/test/browser_test_utils.ccchrome/browser/glic/host/glic_drag_and_drop_browsertest.cccontent/browser/web_contents/web_contents_view_aura_unittest.ccchrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cccontent/browser/web_contents/web_contents_view_aura.hcontent/browser/web_contents/web_contents_view_aura.cc
Audit Directions
- Renderer-supplied coordinates reaching OS inputAudit every path where a value originating in the renderer (locations, points, offsets in
event_info-style structs) flows into platform input or drag APIs likeDragDropClient::StartDragAndDropor::SendInput, and confirm it is replaced with a browser-observedaura::Envvalue. - Asymmetric input-source handlingLook for code that hardens one
DragEventSource/input source (e.g.kTouch) but leaves a sibling source (e.g.kMouse) on the untrusted path, since parity gaps like this one are a recurring source class. - Missing "input actually happened" preconditionsFlag operations that synthesize or forward user-style input without first checking real browser-observed input state (
is_touch_down(),IsMouseButtonDown()), as their absence lets a renderer fabricate gestures.
Patch
From 7db9980b4ebffff30d751384e3fb82939df3cf9c Mon Sep 17 00:00:00 2001 From: David Bienvenu <[email protected]> Date: Thu, 30 Jul 2026 13:42:22 -0700 Subject: [PATCH] clamp mouse location for renderer initiated drag drop touch initiated drag drops already have their location clamped. Do the same for mouse initiated drag drops. Change the test that makes sure a drag from a location outside of the web contents is rejected to test instead that the drag location is clamped to the last mouse down position. Bug: 521616899 Change-Id: I11a85f1f94f9c94dfe71ef13646699e2f674bbf1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7921191 Reviewed-by: Avi Drissman <[email protected]> Reviewed-by: Alex Moshchuk <[email protected]> Commit-Queue: David Bienvenu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1671382} --- diff --git a/content/browser/web_contents/web_contents_view_aura.cc b/content/browser/web_contents/web_contents_view_aura.cc index 5e2ace60d..e4b67bc 100644 --- a/content/browser/web_contents/web_contents_view_aura.cc +++ b/content/browser/web_contents/web_contents_view_aura.cc @@ -1260,22 +1260,27 @@ DragOperation result_op; { gfx::NativeView content_native_view = GetContentNativeView(); - // For a touch-initiated drag the renderer-supplied `event_info.location` - // is untrusted: on Windows it would reach `::SendInput` via + // The renderer-supplied `event_info.location` is untrusted. On Windows, a + // touch event would reach `::SendInput` via // DesktopWindowTreeHostWin::StartTouchDrag and could redirect the // synthesized click to an overlapping HWND (e.g. a permission bubble). - // Require an in-flight touch and substitute the browser-observed last - // touch point known to aura::Env. + // Require an in-flight touch or mouse button, depending on the event + // source, and refuse the drag if requirement is not met. gfx::Point trusted_location = event_info.location; + aura::Env* env = aura::Env::GetInstance(); + if ((event_info.source == ui::mojom::DragEventSource::kTouch && + !env->is_touch_down()) || + (event_info.source == ui::mojom::DragEventSource::kMouse && + !env->IsMouseButtonDown())) { + web_contents_->SystemDragEnded(source_rwh); + return; + } if (event_info.source == ui::mojom::DragEventSource::kTouch) { - aura::Env* env = aura::Env::GetInstance(); - if (!env->is_touch_down()) { - web_contents_->SystemDragEnded(source_rwh); - return; - } trusted_location = env->GetLastPointerPoint(event_info.source, content_native_view, /*fallback=*/event_info.location); + } else { + trusted_location = env->last_mouse_location(); } // Make sure event is within the web contents, and the web contents are // visible. diff --git a/content/browser/web_contents/web_contents_view_aura.h b/content/browser/web_contents/web_contents_view_aura.h index 8ff15a5..5e296e0 100644 --- a/content/browser/web_contents/web_contents_view_aura.h +++ b/content/browser/web_contents/web_contents_view_aura.h @@ -185,7 +185,8 @@ EmptyTextWithUrlInDropDataIsEmptyInOSExchangeDataGetString); FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, RejectDragFromHiddenWebContents); - FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, RejectDragFromOutsideView); + FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, + ClampMouseLocationToBrowserObservedPoint); FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, ClampTouchLocationToBrowserObservedPoint); FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, diff --git a/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc b/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc index 61b769bc..b08859b 100644 --- a/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc +++ b/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc @@ -264,9 +264,6 @@ content::SimulateMouseEvent( source_wc, blink::WebInputEvent::Type::kMouseMove, blink::WebMouseEvent::Button::kLeft, drag_end_point); - content::SimulateMouseEvent(source_wc, blink::WebInputEvent::Type::kMouseUp, - blink::WebMouseEvent::Button::kLeft, - drag_end_point); } private: diff --git a/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc b/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc index e3fa608..6e59c8b 100644 --- a/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc +++ b/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc @@ -61,10 +61,14 @@ #include "content/public/test/test_navigation_observer.h" #include "ui/aura/client/drag_drop_client.h" #include "ui/aura/client/drag_drop_client_observer.h" -#include "ui/base/clipboard//clipboard_monitor.h" -#include "ui/base/clipboard//clipboard_observer.h" +#include "ui/aura/env.h" +#include "ui/aura/input_state_lookup.h" +#include "ui/aura/test/env_test_helper.h" +#include "ui/aura/window_observer.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/clipboard_buffer.h" +#include "ui/base/clipboard/clipboard_monitor.h" +#include "ui/base/clipboard/clipboard_observer.h" #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/base/interaction/element_identifier.h" @@ -1088,18 +1092,32 @@ #endif // BUILDFLAG(IS_MAC) #if defined(USE_AURA) -class TestDragDropClient : public aura::client::DragDropClient { +class TestDragDropClient : public aura::client::DragDropClient, + public aura::WindowObserver { public: explicit TestDragDropClient(aura::Window* root_window) : root_window_(root_window) { - client_ = aura::client::GetDragDropClient(root_window_); - aura::client::SetDragDropClient(root_window_, this); + if (root_window_) { + root_window_->AddObserver(this); + client_ = aura::client::GetDragDropClient(root_window_); + aura::client::SetDragDropClient(root_window_, this); + } } ~TestDragDropClient() override { - for (auto& observer : observers_) { - observer.OnDragDropClientDestroying(); + if (root_window_) { + root_window_->RemoveObserver(this); + for (auto& observer : observers_) { + observer.OnDragDropClientDestroying(); + } + aura::client::SetDragDropClient(root_window_, client_); } - aura::client::SetDragDropClient(root_window_, client_); + } + + void OnWindowDestroying(aura::Window* window) override { + if (window == root_window_) { + root_window_->RemoveObserver(this); + root_window_ = nullptr; + } } // aura::client::DragDropClient: @@ -1233,12 +1251,17 @@ ->GetNativeWindow() ->GetRootWindow(); drag_drop_client_ = std::make_unique<TestDragDropClient>(root_window); + aura::test::EnvTestHelper(aura::Env::GetInstance()) + .SetInputStateLookup(nullptr); }))); } MultiStep ResetDragDropClient() { - return Steps(Do( - base::BindLambdaForTesting([this]() { drag_drop_client_.reset(); }))); + return Steps(Do(base::BindLambdaForTesting([this]() { + aura::test::EnvTestHelper(aura::Env::GetInstance()) + .SetInputStateLookup(aura::InputStateLookup::Create()); + drag_drop_client_.reset(); + }))); } std::unique_ptr<TestDragDropClient> drag_drop_client_; @@ -1275,14 +1298,17 @@ ->GetNativeWindow() ->GetRootWindow(); drag_drop_client = std::make_unique<TestDragDropClient>(root_window); + aura::test::EnvTestHelper(aura::Env::GetInstance()) + .SetInputStateLookup(nullptr); })), // Move to icon and perform drag gesture. MoveMouseTo(WebUIToolbarId(), kLocationIconDeepQuery), DragMouseTo(base::BindOnce([]() -> gfx::Point { - return display::Screen::Get()->GetCursorScreenPoint() + - gfx::Vector2d(0, 20); - })), + return display::Screen::Get()->GetCursorScreenPoint() + + gfx::Vector2d(0, 20); + }), + /*release=*/false), // Verify that drag was triggered with correct data. PollUntil(base::BindLambdaForTesting([&]() { @@ -1291,8 +1317,14 @@ }), "Drag was triggered with correct URL"), + ReleaseMouse(), + // Cleanup. - Do(base::BindLambdaForTesting([&]() { drag_drop_client.reset(); }))); + Do(base::BindLambdaForTesting([&]() { + aura::test::EnvTestHelper(aura::Env::GetInstance()) + .SetInputStateLookup(aura::InputStateLookup::Create()); + drag_drop_client.reset(); + }))); #endif // defined(USE_AURA) && !BUILDFLAG(IS_CHROMEOS) } @@ -1349,7 +1381,7 @@ MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery), // Drag to a sibling Views-level element to trigger the move. - DragMouseTo(kReloadButtonElementId), + DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false), PollUntil(base::BindLambdaForTesting([&]() { return drag_drop_client_->drag_triggered() && @@ -1358,6 +1390,8 @@ }), "Drag was triggered with correct plain text"), + ReleaseMouse(), + ResetDragDropClient()); #endif } @@ -1402,7 +1436,7 @@ MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery), // Drag to a sibling Views-level element to trigger the move. - DragMouseTo(kReloadButtonElementId), + DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false), PollUntil(base::BindLambdaForTesting([&]() { return drag_drop_client_->drag_triggered() && @@ -1412,6 +1446,8 @@ }), "Drag was triggered with correct URL and plain text"), + ReleaseMouse(), + ResetDragDropClient()); #endif } @@ -1450,7 +1486,7 @@ MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery), // Drag to a sibling Views-level element to trigger the move. - DragMouseTo(kReloadButtonElementId), + DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false), PollUntil(base::BindLambdaForTesting([&]() { return drag_drop_client_->drag_triggered() && @@ -1460,6 +1496,8 @@ }), "Drag was triggered with javascript as plain text only"), + ReleaseMouse(), + ResetDragDropClient()); #endif } @@ -1498,7 +1536,7 @@ MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery), // Drag to a sibling Views-level element to trigger the move. - DragMouseTo(kReloadButtonElementId), + DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false), // Verify that chrome:// URL is dragged as a URL. PollUntil(base::BindLambdaForTesting([&]() { @@ -1510,6 +1548,8 @@ }), "Drag was triggered with chrome URL"), + ReleaseMouse(), + ResetDragDropClient()); #endif } @@ -1548,7 +1588,7 @@ MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery), // Drag to a sibling Views-level element to trigger the move. - DragMouseTo(kReloadButtonElementId), + DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false), // Verify that the adjusted URL is dragged. PollUntil( @@ -1567,6 +1607,8 @@ "Drag was triggered with partial selection adjusted to full GURL and " "plain text"), + ReleaseMouse(), + ResetDragDropClient()); #endif } diff --git a/content/browser/web_contents/web_contents_view_aura_unittest.cc b/content/browser/web_contents/web_contents_view_aura_unittest.cc index bd41996..20e6442 100644 --- a/content/browser/web_contents/web_contents_view_aura_unittest.cc +++ b/content/browser/web_contents/web_contents_view_aura_unittest.cc @@ -28,6 +28,7 @@ #include "ui/aura/client/aura_constants.h" #include "ui/aura/client/drag_drop_client.h" #include "ui/aura/env.h" +#include "ui/aura/test/env_test_helper.h" #include "ui/aura/test/test_windows.h" #include "ui/aura/test/window_test_api.h" #include "ui/aura/window.h" @@ -198,10 +199,20 @@ .window_type = aura::client::WINDOW_TYPE_NORMAL, .window_id = 0, .show = false}); + // Force Env's IsMouseButtonDown to rely on mouse_button_flags_ instead of + // querying the native OS system (which would return false in headless/unit + // tests). + aura::test::EnvTestHelper(aura::Env::GetInstance()) + .SetInputStateLookup(nullptr); + aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON); } void TearDown() override { occluding_window_.reset(); + aura::Env::GetInstance()->SetLastMouseLocation(gfx::Point()); + aura::test::EnvTestHelper(aura::Env::GetInstance()) + .SetInputStateLookup(aura::InputStateLookup::Create()); + aura::Env::GetInstance()->set_mouse_button_flags(0); RenderViewHostTestHarness::TearDown(); } @@ -220,7 +231,8 @@ drop_complete_data_->target_rwh.get()); EXPECT_EQ(kClientPt, drop_complete_data_->client_pt); // Screen point of event is ignored, instead cursor position used. - EXPECT_EQ(gfx::PointF(), drop_complete_data_->screen_pt); + EXPECT_EQ(gfx::PointF(aura::Env::GetInstance()->last_mouse_location()), + drop_complete_data_->screen_pt); EXPECT_EQ(0, drop_complete_data_->key_modifiers); } @@ -1075,6 +1087,8 @@ view->drag_in_progress_ = true; DropData drop_data; + aura::Env::GetInstance()->SetLastMouseLocation( + view->GetContentNativeView()->GetBoundsInScreen().CenterPoint()); view->StartDragging(*main_rfh(), drop_data, blink::DragOperationsMask::kDragOperationNone, gfx::ImageSkia(), gfx::Vector2d(), gfx::Rect(), @@ -1111,9 +1125,10 @@ EXPECT_FALSE(exchange_data); } -// If the event location is not in the WebContentsViewAura, the drag will not be -// started. -TEST_F(WebContentsViewAuraTest, RejectDragFromOutsideView) { +// For a mouse-initiated drag, the renderer-supplied screen location must +// not flow through to DragDropClient::StartDragAndDrop. Instead the trusted +// browser-observed last mouse location (aura::Env) must be used. +TEST_F(WebContentsViewAuraTest, ClampMouseLocationToBrowserObservedPoint) { const char kGoogleUrl[] = "https://google.com/"; std::u16string url_string = u"https://google.com/"; @@ -1132,6 +1147,15 @@ DropData drop_data; drop_data.url_infos = {ui::ClipboardUrlInfo{GURL(kGoogleUrl), u""}}; + // This condition is needed to avoid calling WebContentsViewAura::EndDrag + // which will result NOTREACHED being called in + // `RenderWidgetHostViewBase::TransformPointToCoordSpaceForView`. + view->drag_in_progress_ = true; + + const gfx::Point trusted_location(view_bounds_on_screen.x() + 3, + view_bounds_on_screen.y() + 4); + aura::Env::GetInstance()->SetLastMouseLocation(trusted_location); + view->StartDragging( *main_rfh(), drop_data, blink::DragOperationsMask::kDragOperationNone, gfx::ImageSkia(), gfx::Vector2d(), gfx::Rect(), @@ -1141,7 +1165,11 @@ ui::mojom::DragEventSource::kMouse)); ui::OSExchangeData* exchange_data = drag_drop_client.GetDragDropData(); - EXPECT_FALSE(exchange_data); + EXPECT_TRUE(exchange_data); + EXPECT_EQ(ui::mojom::DragEventSource::kMouse, drag_drop_client.last_source()); + EXPECT_EQ(trusted_location, drag_drop_client.last_screen_location()) + << "Renderer-supplied screen location must be clamped to the " + "browser-observed last mouse point."; } // For a touch-initiated drag, the renderer-supplied screen location must @@ -1205,6 +1233,8 @@ DropData drop_data; drop_data.text = empty_string; + aura::Env::GetInstance()->SetLastMouseLocation( + view->GetContentNativeView()->GetBoundsInScreen().CenterPoint()); view->StartDragging(*main_rfh(), drop_data, blink::DragOperationsMask::kDragOperationNone, gfx::ImageSkia(), gfx::Vector2d(), gfx::Rect(), ... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc b/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc
index 61b769bc..b08859b 100644
--- a/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc
+++ b/chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc
@@ -264,9 +264,6 @@
content::SimulateMouseEvent(
source_wc, blink::WebInputEvent::Type::kMouseMove,
blink::WebMouseEvent::Button::kLeft, drag_end_point);
- content::SimulateMouseEvent(source_wc, blink::WebInputEvent::Type::kMouseUp,
- blink::WebMouseEvent::Button::kLeft,
- drag_end_point);
}
private:
diff --git a/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc b/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
index e3fa608..6e59c8b 100644
--- a/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
+++ b/chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
@@ -61,10 +61,14 @@
#include "content/public/test/test_navigation_observer.h"
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/client/drag_drop_client_observer.h"
-#include "ui/base/clipboard//clipboard_monitor.h"
-#include "ui/base/clipboard//clipboard_observer.h"
+#include "ui/aura/env.h"
+#include "ui/aura/input_state_lookup.h"
+#include "ui/aura/test/env_test_helper.h"
+#include "ui/aura/window_observer.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_buffer.h"
+#include "ui/base/clipboard/clipboard_monitor.h"
+#include "ui/base/clipboard/clipboard_observer.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/interaction/element_identifier.h"
@@ -1088,18 +1092,32 @@
#endif // BUILDFLAG(IS_MAC)
#if defined(USE_AURA)
-class TestDragDropClient : public aura::client::DragDropClient {
+class TestDragDropClient : public aura::client::DragDropClient,
+ public aura::WindowObserver {
public:
explicit TestDragDropClient(aura::Window* root_window)
: root_window_(root_window) {
- client_ = aura::client::GetDragDropClient(root_window_);
- aura::client::SetDragDropClient(root_window_, this);
+ if (root_window_) {
+ root_window_->AddObserver(this);
+ client_ = aura::client::GetDragDropClient(root_window_);
+ aura::client::SetDragDropClient(root_window_, this);
+ }
}
~TestDragDropClient() override {
- for (auto& observer : observers_) {
- observer.OnDragDropClientDestroying();
+ if (root_window_) {
+ root_window_->RemoveObserver(this);
+ for (auto& observer : observers_) {
+ observer.OnDragDropClientDestroying();
+ }
+ aura::client::SetDragDropClient(root_window_, client_);
}
- aura::client::SetDragDropClient(root_window_, client_);
+ }
+
+ void OnWindowDestroying(aura::Window* window) override {
+ if (window == root_window_) {
+ root_window_->RemoveObserver(this);
+ root_window_ = nullptr;
+ }
}
// aura::client::DragDropClient:
@@ -1233,12 +1251,17 @@
->GetNativeWindow()
->GetRootWindow();
drag_drop_client_ = std::make_unique<TestDragDropClient>(root_window);
+ aura::test::EnvTestHelper(aura::Env::GetInstance())
+ .SetInputStateLookup(nullptr);
})));
}
MultiStep ResetDragDropClient() {
- return Steps(Do(
- base::BindLambdaForTesting([this]() { drag_drop_client_.reset(); })));
+ return Steps(Do(base::BindLambdaForTesting([this]() {
+ aura::test::EnvTestHelper(aura::Env::GetInstance())
+ .SetInputStateLookup(aura::InputStateLookup::Create());
+ drag_drop_client_.reset();
+ })));
}
std::unique_ptr<TestDragDropClient> drag_drop_client_;
@@ -1275,14 +1298,17 @@
->GetNativeWindow()
->GetRootWindow();
drag_drop_client = std::make_unique<TestDragDropClient>(root_window);
+ aura::test::EnvTestHelper(aura::Env::GetInstance())
+ .SetInputStateLookup(nullptr);
})),
// Move to icon and perform drag gesture.
MoveMouseTo(WebUIToolbarId(), kLocationIconDeepQuery),
DragMouseTo(base::BindOnce([]() -> gfx::Point {
- return display::Screen::Get()->GetCursorScreenPoint() +
- gfx::Vector2d(0, 20);
- })),
+ return display::Screen::Get()->GetCursorScreenPoint() +
+ gfx::Vector2d(0, 20);
+ }),
+ /*release=*/false),
// Verify that drag was triggered with correct data.
PollUntil(base::BindLambdaForTesting([&]() {
@@ -1291,8 +1317,14 @@
}),
"Drag was triggered with correct URL"),
+ ReleaseMouse(),
+
// Cleanup.
- Do(base::BindLambdaForTesting([&]() { drag_drop_client.reset(); })));
+ Do(base::BindLambdaForTesting([&]() {
+ aura::test::EnvTestHelper(aura::Env::GetInstance())
+ .SetInputStateLookup(aura::InputStateLookup::Create());
+ drag_drop_client.reset();
+ })));
#endif // defined(USE_AURA) && !BUILDFLAG(IS_CHROMEOS)
}
@@ -1349,7 +1381,7 @@
MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery),
// Drag to a sibling Views-level element to trigger the move.
- DragMouseTo(kReloadButtonElementId),
+ DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false),
PollUntil(base::BindLambdaForTesting([&]() {
return drag_drop_client_->drag_triggered() &&
@@ -1358,6 +1390,8 @@
}),
"Drag was triggered with correct plain text"),
+ ReleaseMouse(),
+
ResetDragDropClient());
#endif
}
@@ -1402,7 +1436,7 @@
MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery),
// Drag to a sibling Views-level element to trigger the move.
- DragMouseTo(kReloadButtonElementId),
+ DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false),
PollUntil(base::BindLambdaForTesting([&]() {
return drag_drop_client_->drag_triggered() &&
@@ -1412,6 +1446,8 @@
}),
"Drag was triggered with correct URL and plain text"),
+ ReleaseMouse(),
+
ResetDragDropClient());
#endif
}
@@ -1450,7 +1486,7 @@
MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery),
// Drag to a sibling Views-level element to trigger the move.
- DragMouseTo(kReloadButtonElementId),
+ DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false),
PollUntil(base::BindLambdaForTesting([&]() {
return drag_drop_client_->drag_triggered() &&
@@ -1460,6 +1496,8 @@
}),
"Drag was triggered with javascript as plain text only"),
+ ReleaseMouse(),
+
ResetDragDropClient());
#endif
}
@@ -1498,7 +1536,7 @@
MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery),
// Drag to a sibling Views-level element to trigger the move.
- DragMouseTo(kReloadButtonElementId),
+ DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false),
// Verify that chrome:// URL is dragged as a URL.
PollUntil(base::BindLambdaForTesting([&]() {
@@ -1510,6 +1548,8 @@
}),
"Drag was triggered with chrome URL"),
+ ReleaseMouse(),
+
ResetDragDropClient());
#endif
}
@@ -1548,7 +1588,7 @@
MoveMouseTo(WebUIToolbarId(), kTextSpanDeepQuery),
// Drag to a sibling Views-level element to trigger the move.
- DragMouseTo(kReloadButtonElementId),
+ DragMouseTo(kReloadButtonElementId, CenterPoint(), /*release=*/false),
// Verify that the adjusted URL is dragged.
PollUntil(
@@ -1567,6 +1607,8 @@
"Drag was triggered with partial selection adjusted to full GURL and "
"plain text"),
+ ReleaseMouse(),
+
ResetDragDropClient());
#endif
}
diff --git a/content/browser/web_contents/web_contents_view_aura_unittest.cc b/content/browser/web_contents/web_contents_view_aura_unittest.cc
index bd41996..20e6442 100644
--- a/content/browser/web_contents/web_contents_view_aura_unittest.cc
+++ b/content/browser/web_contents/web_contents_view_aura_unittest.cc
@@ -28,6 +28,7 @@
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/env.h"
+#include "ui/aura/test/env_test_helper.h"
#include "ui/aura/test/test_windows.h"
#include "ui/aura/test/window_test_api.h"
#include "ui/aura/window.h"
@@ -198,10 +199,20 @@
.window_type = aura::client::WINDOW_TYPE_NORMAL,
.window_id = 0,
.show = false});
+ // Force Env's IsMouseButtonDown to rely on mouse_button_flags_ instead of
+ // querying the native OS system (which would return false in headless/unit
+ // tests).
+ aura::test::EnvTestHelper(aura::Env::GetInstance())
+ .SetInputStateLookup(nullptr);
+ aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
}
void TearDown() override {
occluding_window_.reset();
+ aura::Env::GetInstance()->SetLastMouseLocation(gfx::Point());
+ aura::test::EnvTestHelper(aura::Env::GetInstance())
+ .SetInputStateLookup(aura::InputStateLookup::Create());
+ aura::Env::GetInstance()->set_mouse_button_flags(0);
RenderViewHostTestHarness::TearDown();
}
@@ -220,7 +231,8 @@
drop_complete_data_->target_rwh.get());
EXPECT_EQ(kClientPt, drop_complete_data_->client_pt);
// Screen point of event is ignored, instead cursor position used.
- EXPECT_EQ(gfx::PointF(), drop_complete_data_->screen_pt);
+ EXPECT_EQ(gfx::PointF(aura::Env::GetInstance()->last_mouse_location()),
+ drop_complete_data_->screen_pt);
EXPECT_EQ(0, drop_complete_data_->key_modifiers);
}
@@ -1075,6 +1087,8 @@
view->drag_in_progress_ = true;
DropData drop_data;
+ aura::Env::GetInstance()->SetLastMouseLocation(
+ view->GetContentNativeView()->GetBoundsInScreen().CenterPoint());
view->StartDragging(*main_rfh(), drop_data,
blink::DragOperationsMask::kDragOperationNone,
gfx::ImageSkia(), gfx::Vector2d(), gfx::Rect(),
@@ -1111,9 +1125,10 @@
EXPECT_FALSE(exchange_data);
}
-// If the event location is not in the WebContentsViewAura, the drag will not be
-// started.
-TEST_F(WebContentsViewAuraTest, RejectDragFromOutsideView) {
+// For a mouse-initiated drag, the renderer-supplied screen location must
+// not flow through to DragDropClient::StartDragAndDrop. Instead the trusted
+// browser-observed last mouse location (aura::Env) must be used.
+TEST_F(WebContentsViewAuraTest, ClampMouseLocationToBrowserObservedPoint) {
const char kGoogleUrl[] = "https://google.com/";
std::u16string url_string = u"https://google.com/";
@@ -1132,6 +1147,15 @@
DropData drop_data;
drop_data.url_infos = {ui::ClipboardUrlInfo{GURL(kGoogleUrl), u""}};
+ // This condition is needed to avoid calling WebContentsViewAura::EndDrag
+ // which will result NOTREACHED being called in
+ // `RenderWidgetHostViewBase::TransformPointToCoordSpaceForView`.
+ view->drag_in_progress_ = true;
+
+ const gfx::Point trusted_location(view_bounds_on_screen.x() + 3,
+ view_bounds_on_screen.y() + 4);
+ aura::Env::GetInstance()->SetLastMouseLocation(trusted_location);
+
... (truncated)
Original Bug Report
Potential bypass of drag-and-drop guards during title-bar click-hold on Windows
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: Chrome’s Windows non-client mouse handler defers processing WM_NCLBUTTONDOWN for HTCAPTION until a mouse move occurs to keep the UI responsive during a click-hold. During this deferral, neither the native move-resize loop nor menu loop flags are set, which allows a compromised renderer to bypass drag guards. An attacker can initiate an arbitrary drag-and-drop session with controlled payload while the user is click-holding the title bar.
Affected files:
ui/views/win/hwnd_message_handler.ccui/views/widget/desktop_aura/desktop_drag_drop_client_win.ccui/views/win/hwnd_message_handler.h
Estimated timestamp from git blame: 2015-12-11
Root Cause Analysis
When a user left-clicks and holds on the title bar (caption area) of a top-level Chrome window on Windows, the message WM_NCLBUTTONDOWN with wParam == HTCAPTION is generated. To keep page and Mojo dispatch responsive during a click-hold, HWNDMessageHandler::HandleMouseInputForCaption deliberately intercepts and handles this message:
// ui/views/win/hwnd_message_handler.cc
case WM_NCLBUTTONDOWN: {
if (w_param == HTCAPTION) {
left_button_down_on_caption_ = true;
caption_left_button_click_pos_.set_x(CR_GET_X_LPARAM(l_param));
caption_left_button_click_pos_.set_y(CR_GET_Y_LPARAM(l_param));
handled = true; // DefWindowProc is NOT called here
}
break;
}
Because handled is set to true, the default window procedure (::DefWindowProc) is suppressed. As a result, the OS does not enter the native move/resize loop, meaning:
WM_ENTERSIZEMOVEis never dispatched.WM_MOVINGandWM_SIZINGdo not fire, leavingg_in_move_resize_loopset tofalseinUserResizeMoveDetector.menu_depth_remains0as no menu loop is entered.
The ordinary UI message pump continues to run normally and processes Mojo IPCs from the renderer.
Normally, DesktopDragDropClientWin::StartDragAndDrop relies on desktop_host_->IsInNativeMoveResizeLoop() to reject dragging requests if the user is in the middle of a window drag or resize operation. However, during the HTCAPTION hold deferral, IsInNativeMoveResizeLoop() returns false since none of the native loops have technically started yet, and the private flag left_button_down_on_caption_ is not checked by the drag client. This allows the compromised renderer to successfully start an OLE drag-and-drop loop via ::DoDragDrop.
Since the physical mouse button is still held down by the user, DragSourceWin::QueryContinueDrag returns S_OK to continue dragging. When the user moves the mouse (intending to move the window), they instead drag the attacker-supplied payload and drop it wherever they release the button.
Potential Attack Path
Note: These are potential steps as our tooling agent does not currently have the ability to execute code and we do not have a working proof of concept.
- The user left-clicks and holds (without moving) on the empty caption/title bar of any Chrome window.
HWNDMessageHandler::HandleMouseInputForCaptionhandles the message, setsleft_button_down_on_caption_ = true, and suppresses the default window procedure. The ordinary UI message pump continues running.- While the button is held, a compromised renderer sends a Mojo message
LocalFrameHost::StartDraggingcontaining an attacker-controlled payload (e.g., ajavascript:URL bookmarklet or a virtual file) withevent_info.sourceset tokMouse. - The browser process receives this and calls
WebContentsViewAura::StartDragging. The renderer satisfies the bounds check by supplying a coordinate location inside its own visible WebContents. WebContentsViewAura::StartDraggingcallsDesktopDragDropClientWin::StartDragAndDrop.- The client evaluates
IsInNativeMoveResizeLoop(), which returnsfalsebecause no native move/resize or menu loops are active, bypassing the drag guards. ::DoDragDropis called with the attacker payload. The user’s hold keeps the drag active viaQueryContinueDragreturningS_OKforMK_LBUTTON.- The user moves and releases the mouse, dropping the attacker-controlled payload onto an arbitrary drop target (such as Windows Explorer, another Chrome window, or the bookmarks bar).
Suggested Remediation
To prevent this bypass, DesktopDragDropClientWin::StartDragAndDrop should reject mouse-initiated drag requests unless the browser has itself observed a left-button press captured within the initiating WebContents (similar to how touch drags are verified via is_touch_down()). Alternatively, HWNDMessageHandler should expose the state of left_button_down_on_caption_ so it can be OR’ed into the IsInNativeMoveResizeLoop() guard predicate in DesktopWindowTreeHostWin::IsInNativeMoveResizeLoop().
Evaluated with Chrome root at commit: 3947e01999a53d4e2382e39736cb79d79c7dffcf
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.