Medium watch CVSS 8.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Views
DescriptionIncorrect authorization in Views
ComponentViews
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker521616899
Fix commit7db9980b4ebf (chromium/src) +179/-37
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-09 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

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(...), and last_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.

Key insight
The core mistake was trusting the renderer-supplied 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

  1. Compromised renderer starts a mouse drag A malicious or compromised renderer issues a renderer-initiated drag with source == ui::mojom::DragEventSource::kMouse and no genuine user mouse-button press behind it.
  2. Attacker-chosen location is supplied The renderer sets event_info.location to an arbitrary screen point of its choosing rather than the real cursor position.
  3. Unclamped location reaches the platform Pre-patch WebContentsViewAura::StartDragging keeps trusted_location = event_info.location for mouse and passes it to DragDropClient::StartDragAndDrop, so the renderer-controlled point drives the OS drag.
  4. 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.
  5. 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

A compromised renderer gains the ability to spoof the screen coordinates that a mouse-initiated drag delivers to the OS drag-and-drop stack in the browser process, letting synthesized input land on browser UI outside the web contents rather than at the true cursor location. The precondition is a renderer that can issue drag requests (i.e. an already-compromised or malicious renderer) on an Aura platform such as Windows; the escalation to actuating a specific permission bubble or other privileged control is (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

FunctionChangeNotes
if
content/browser/web_contents/web_contents_view_aura.cc
modified
TestDragDropClient
chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
modified
if
chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
modified
for
chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
modified
BindLambdaForTesting
chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
modified
PollUntil
chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
modified
Do
chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
modified
TEST_F
content/browser/web_contents/web_contents_view_aura_unittest.cc
modified

Files Changed

  • ui/aura/test/env_test_helper.h
  • chrome/browser/ui/views/toolbar/BUILD.gn
  • content/public/test/browser_test_utils.cc
  • chrome/browser/glic/host/glic_drag_and_drop_browsertest.cc
  • content/browser/web_contents/web_contents_view_aura_unittest.cc
  • chrome/browser/ui/views/toolbar/webui_toolbar_interactive_uitest.cc
  • content/browser/web_contents/web_contents_view_aura.h
  • content/browser/web_contents/web_contents_view_aura.cc

Audit Directions

  • Renderer-supplied coordinates reaching OS input
    Audit every path where a value originating in the renderer (locations, points, offsets in event_info-style structs) flows into platform input or drag APIs like DragDropClient::StartDragAndDrop or ::SendInput, and confirm it is replaced with a browser-observed aura::Env value.
  • Asymmetric input-source handling
    Look 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" preconditions
    Flag 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.
From 7db9980b4ebffff30d751384e3fb82939df3cf9c Mon Sep 17 00:00:00 2001
From: David Bienvenu <davidbienvenu@chromium.org>
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 <avi@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Commit-Queue: David Bienvenu <davidbienvenu@chromium.org>
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)
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc
  • ui/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_ENTERSIZEMOVE is never dispatched.
  • WM_MOVING and WM_SIZING do not fire, leaving g_in_move_resize_loop set to false in UserResizeMoveDetector.
  • menu_depth_ remains 0 as 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.

  1. The user left-clicks and holds (without moving) on the empty caption/title bar of any Chrome window.
  2. HWNDMessageHandler::HandleMouseInputForCaption handles the message, sets left_button_down_on_caption_ = true, and suppresses the default window procedure. The ordinary UI message pump continues running.
  3. While the button is held, a compromised renderer sends a Mojo message LocalFrameHost::StartDragging containing an attacker-controlled payload (e.g., a javascript: URL bookmarklet or a virtual file) with event_info.source set to kMouse.
  4. 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.
  5. WebContentsViewAura::StartDragging calls DesktopDragDropClientWin::StartDragAndDrop.
  6. The client evaluates IsInNativeMoveResizeLoop(), which returns false because no native move/resize or menu loops are active, bypassing the drag guards.
  7. ::DoDragDrop is called with the attacker payload. The user’s hold keeps the drag active via QueryContinueDrag returning S_OK for MK_LBUTTON.
  8. 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.

View on issue tracker