CVE-2026-113068
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/input/event_handler_test.cc |
modified |
Files Changed
content/test/content_test_bundle_data.filelistthird_party/blink/renderer/core/input/event_handler_test.ccthird_party/blink/renderer/core/input/event_handler.ccthird_party/blink/web_tests/fast/events/mouse-cursor.htmlcontent/test/data/small-cursor.htmlthird_party/blink/web_tests/fast/events/mouse-cursor-image-set.htmlcontent/browser/site_per_process_hit_test_browsertest.ccthird_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html
Patch
From 0f7c6587928e64791a3beebb804b686ea2c508f7 Mon Sep 17 00:00:00 2001
From: Mike Wasserman <msw@chromium.org>
Date: Wed, 16 Sep 2026 06:08:10 -0700
Subject: [PATCH] Enforce visual viewport containment for all custom cursors
Remove the 32px cursor threshold for viewport containment in Blink.
Prevents rendering cursors partly outside the root visual viewport.
Falls back on the chain of specified cursors to fit in the viewport.
Aligns the handling of custom cursors regardless of size.
PSA: https://groups.google.com/a/chromium.org/g/blink-dev/c/vOhCzMDvRb0
Mozilla alignment: https://bugzilla.mozilla.org/show_bug.cgi?id=1804816
TAG=agy
Fixed: 40942045
Change-Id: I336e21c35a12e51fba8e23439ea4b24b53d002b9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8391502
Commit-Queue: Camille Lamy <clamy@chromium.org>
Auto-Submit: Mike Wasserman <msw@chromium.org>
Reviewed-by: Camille Lamy <clamy@chromium.org>
Reviewed-by: Charles Harrison <csharrison@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1698869}
---
diff --git a/third_party/blink/renderer/core/input/event_handler.cc b/third_party/blink/renderer/core/input/event_handler.cc
index 19e2b3d..fc1e894a 100644
--- a/third_party/blink/renderer/core/input/event_handler.cc
+++ b/third_party/blink/renderer/core/input/event_handler.cc
@@ -257,11 +257,6 @@
// Set to 50Hz, no need to be faster than common screen refresh rate
static constexpr base::TimeDelta kCursorUpdateInterval = base::Milliseconds(20);
-// The maximum size a cursor can be without falling back to the default cursor
-// when intersecting browser native UI.
-// https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#icon_size_limits.
-static const int kMaximumCursorSizeWithoutFallback = 32;
-
// The minimum amount of time an element stays active after a ShowPress
// This is roughly 9 frames, which should be long enough to be noticeable.
constexpr base::TimeDelta kMinimumActiveInterval = base::Seconds(0.15);
@@ -718,51 +713,43 @@
image->AsSkBitmapForCurrentFrame(kRespectImageOrientation),
DetermineHotSpot(*image, hot_spot_specified, hot_spot), scale);
- // For large cursors below the max size, limit their ability to cover UI
- // elements by removing them when they are not fully contained by the
- // visual viewport. Careful, we need to make sure to translate coordinate
- // spaces if we are in an OOPIF.
+ // Limit custom cursors from covering UI elements by removing them when
+ // they are not fully contained by the visual viewport. Careful, we need
+ // to make sure to translate coordinate spaces if we are in an OOPIF.
//
// TODO(csharrison): Consider sending a fallback cursor in the IPC to the
- // browser process so we can do that calculation there instead, this would
- // ensure even a compromised renderer could not obscure browser UI with a
- // large cursor. Also, consider augmenting the intervention to drop the
- // cursor for iframes if the cursor image obscures content in the parent
- // frame.
+ // browser process so we can do that calculation there instead. Also,
+ // consider augmenting the intervention to drop the cursor for iframes if
+ // the cursor image obscures content in the parent frame.
gfx::SizeF custom_bitmap_size(custom_cursor.custom_bitmap().width(),
custom_cursor.custom_bitmap().height());
custom_bitmap_size.Scale(1.f / custom_cursor.image_scale_factor());
- if (custom_bitmap_size.width() > kMaximumCursorSizeWithoutFallback ||
- custom_bitmap_size.height() > kMaximumCursorSizeWithoutFallback) {
- PhysicalOffset ancestor_location =
- frame_->ContentLayoutObject()->LocalToAncestorPoint(
- location.Point(),
- nullptr, // no ancestor maps all the way up the hierarchy
- {MapCoordinatesMode::kTraverseDocumentBoundaries,
- MapCoordinatesMode::kApplyRemoteMainFrameTransform});
+ PhysicalOffset ancestor_location =
+ frame_->ContentLayoutObject()->LocalToAncestorPoint(
+ location.Point(),
+ nullptr, // no ancestor maps all the way up the hierarchy
+ {MapCoordinatesMode::kTraverseDocumentBoundaries,
+ MapCoordinatesMode::kApplyRemoteMainFrameTransform});
- // Check the cursor rect with device and accessibility scaling applied.
- const float scale_factor =
- cursor_accessibility_scale_factor_ *
- (image->IsSVGImage() ? 1.f : device_scale_factor);
- gfx::SizeF scaled_size(custom_bitmap_size);
- scaled_size.Scale(scale_factor);
- gfx::PointF scaled_hot_spot(custom_cursor.custom_hotspot());
- scaled_hot_spot.Scale(scale_factor /
- custom_cursor.image_scale_factor());
- PhysicalRect cursor_rect(
- ancestor_location -
- PhysicalOffset::FromPointFFloor(scaled_hot_spot),
- PhysicalSize::FromSizeFFloor(scaled_size));
+ // Check the cursor rect with device and accessibility scaling applied.
+ const float scale_factor =
+ cursor_accessibility_scale_factor_ *
+ (image->IsSVGImage() ? 1.f : device_scale_factor);
+ gfx::SizeF scaled_size(custom_bitmap_size);
+ scaled_size.Scale(scale_factor);
+ gfx::PointF scaled_hot_spot(custom_cursor.custom_hotspot());
+ scaled_hot_spot.Scale(scale_factor / custom_cursor.image_scale_factor());
+ PhysicalRect cursor_rect(
+ ancestor_location - PhysicalOffset::FromPointFFloor(scaled_hot_spot),
+ PhysicalSize::FromSizeFFloor(scaled_size));
- PhysicalRect frame_rect(
- page->GetVisualViewport().VisibleContentRect(kExcludeScrollbars));
- frame_->ContentLayoutObject()->MapToVisualRectInAncestorSpace(
- nullptr, frame_rect);
+ PhysicalRect frame_rect(
+ page->GetVisualViewport().VisibleContentRect(kExcludeScrollbars));
+ frame_->ContentLayoutObject()->MapToVisualRectInAncestorSpace(nullptr,
+ frame_rect);
- if (!frame_rect.Contains(cursor_rect)) {
- continue;
- }
+ if (!frame_rect.Contains(cursor_rect)) {
+ continue;
}
return custom_cursor;
diff --git a/content/browser/site_per_process_hit_test_browsertest.cc b/content/browser/site_per_process_hit_test_browsertest.cc
index 70463c3..478a015 100644
--- a/content/browser/site_per_process_hit_test_browsertest.cc
+++ b/content/browser/site_per_process_hit_test_browsertest.cc
@@ -4513,6 +4513,81 @@
}
}
+// Ensure small custom cursors are not allowed to intersect the root visual
+// viewport.
+IN_PROC_BROWSER_TEST_F(SitePerProcessHitTestBrowserTest,
+ SmallCursorRemovedInOffsetOOPIF) {
+ GURL url(R"(data:text/html,
+ <iframe id='iframe'
+ style ='position:absolute; top: -10px'
+ width=1000px height=1000px>
+ </iframe>)");
+ EXPECT_TRUE(NavigateToURL(shell(), url));
+
+ // The small-cursor.html document has a custom cursor that is 32x32 with a
+ // hotspot on the bottom right corner.
+ NavigateIframeToURL(web_contents(), "iframe",
+ embedded_test_server()->GetURL("/small-cursor.html"));
+
+ FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
+
+ FrameTreeNode* child_node = root->child_at(0);
+ EXPECT_NE(web_contents()->GetSiteInstance(),
+ child_node->current_frame_host()->GetSiteInstance());
+
+ WaitForHitTestData(child_node->current_frame_host());
+
+ RenderWidgetHostViewBase* root_view = static_cast<RenderWidgetHostViewBase*>(
+ root->current_frame_host()->GetRenderWidgetHost()->GetView());
+ RenderWidgetHostImpl* rwh_child =
+ root->child_at(0)->current_frame_host()->GetRenderWidgetHost();
+ RenderWidgetHostViewBase* child_view =
+ static_cast<RenderWidgetHostViewBase*>(rwh_child->GetView());
+
+ auto* router = web_contents()->GetInputEventRouter();
+ RenderWidgetHostMouseEventMonitor child_monitor(
+ child_view->GetRenderWidgetHost());
+ RenderWidgetHostMouseEventMonitor root_monitor(
+ root_view->GetRenderWidgetHost());
+
+ // A cursor with enough room in the root view to fully display without
+ // intersecting native UI should be shown.
+ {
+ blink::WebMouseEvent mouse_event(
+ blink::WebInputEvent::Type::kMouseMove,
+ blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ SetWebEventPositions(&mouse_event, gfx::Point(300, 300), root_view);
+ auto set_cursor_interceptor =
+ std::make_unique<SetCursorInterceptor>(rwh_child);
+ RouteMouseEventAndWaitUntilDispatch(router, root_view, child_view,
+ &mouse_event);
+ set_cursor_interceptor->Wait();
+ EXPECT_TRUE(set_cursor_interceptor->cursor().has_value());
+ EXPECT_EQ(32, set_cursor_interceptor->cursor()->custom_bitmap().width());
+ EXPECT_EQ(32, set_cursor_interceptor->cursor()->custom_bitmap().height());
+ }
+ // A cursor without enough room to be fully enclosed within the root view
+ // should not be shown, even if the iframe is at an offset. The default cursor
+ // should be shown instead.
+ {
+ blink::WebMouseEvent mouse_event(
+ blink::WebInputEvent::Type::kMouseMove,
+ blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ SetWebEventPositions(&mouse_event, gfx::Point(300, 20), root_view);
+ auto set_cursor_interceptor =
+ std::make_unique<SetCursorInterceptor>(rwh_child);
+ RouteMouseEventAndWaitUntilDispatch(router, root_view, child_view,
+ &mouse_event);
+ // We should see a new cursor come in that replaces the custom one.
+ set_cursor_interceptor->Wait();
+ EXPECT_TRUE(set_cursor_interceptor->cursor().has_value());
+ EXPECT_EQ(ui::mojom::CursorType::kPointer,
+ set_cursor_interceptor->cursor()->type());
+ }
+}
+
// Regression test for https://crbug.com/1454515. An OOPIF
// scrolled away from the main document should not allow
// large cursors to intersect browser UI.
diff --git a/content/test/content_test_bundle_data.filelist b/content/test/content_test_bundle_data.filelist
index 5e909d7..a81c94c5 100644
--- a/content/test/content_test_bundle_data.filelist
+++ b/content/test/content_test_bundle_data.filelist
@@ -9466,6 +9466,7 @@
data/site_isolation/xml.octet-stream.mock-http-headers
data/site_isolation/xml.txt
data/site_per_process_main.html
+data/small-cursor.html
data/speech/web_speech_recognition.html
data/stale-script.js
data/stale-script.js.mock-http-headers
diff --git a/content/test/data/small-cursor.html b/content/test/data/small-cursor.html
new file mode 100644
index 0000000..dd63676
--- /dev/null
+++ b/content/test/data/small-cursor.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+body {
+ height: 1000px;
+ width: 1000px;
+ cursor: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32" fill="red"/></svg>') 32 32, auto;
+}
+</style>
diff --git a/third_party/blink/renderer/core/input/event_handler_test.cc b/third_party/blink/renderer/core/input/event_handler_test.cc
index f08d22a3..82c20c39 100644
--- a/third_party/blink/renderer/core/input/event_handler_test.cc
+++ b/third_party/blink/renderer/core/input/event_handler_test.cc
@@ -2544,7 +2544,7 @@
Compositor().BeginFrame();
- // Move the cursor so no part of it intersects the viewport.
+ // Move the cursor so no part of it intersects the viewport boundary.
{
WebMouseEvent mouse_move_event(
WebMouseEvent::Type::kMouseMove, gfx::PointF(25, 25),
@@ -2558,9 +2558,23 @@
EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
}
- // Now, move the cursor so that it intersects the visual viewport. The cursor
- // should not be removed because it is below
- // kMaximumCursorSizeWithoutFallback.
+ // Move the cursor exactly on the boundary so it is still contained within
+ // the visual viewport.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(24, 24),
+ gfx::PointF(24, 24), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
+ }
+
+ // Now, move the cursor so that it intersects the visual viewport boundary.
+ // The cursor should be removed and fall back to the default pointer.
{
WebMouseEvent mouse_move_event(
WebMouseEvent::Type::kMouseMove, gfx::PointF(23, 23),
@@ -2571,8 +2585,83 @@
const ui::Cursor& cursor =
GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kPointer, cursor.type());
+ }
+}
+
+TEST_F(EventHandlerSimTest, CustomCursor32x32IntersectsViewport) {
+ WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+ SimRequest request("https://example.com/test.html", "text/html");
+ SimSubresourceRequest cursor_request("https://example.com/32x32.svg",
+ "image/svg+xml");
+ LoadURL("https://example.com/test.html");
+ request.Complete(
+ R"HTML(
+ <!DOCTYPE html>
+ <style>
+ div {
+ width: 300px;
+ height: 100px;
+ cursor: url('32x32.svg') 32 32, auto;
+ }
+ </style>
+ <div>foo</div>
+ )HTML");
+
+ GetDocument().UpdateStyleAndLayoutTree();
+
+ cursor_request.Complete(R"SVG(
+ <svg xmlns="http://www.w3.org/2000/svg" width="32px" height="32px">
+ <rect width="32" height="32" fill="red"/>
+ </svg>
+ )SVG");
+
+ Compositor().BeginFrame();
+
+ // Move the cursor so it is fully contained within the visual viewport.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(32, 32),
+ gfx::PointF(32, 32), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
}
+
+ // Move the cursor so that its top-left corner is at (0, 0) in the visual
+ // viewport. Hotspot 32 32 is clamped to (31, 31), so at Point(31, 31),
+ // the cursor rect starts at (0, 0) and is fully contained.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(31, 31),
+ gfx::PointF(31, 31), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
+ }
+
+ // Move the cursor so that it extends beyond the visual viewport boundary.
+ // The cursor should be removed and fall back to the default pointer.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(30, 30),
+ gfx::PointF(30, 30), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kPointer, cursor.type());
+ }
}
TEST_F(EventHandlerSimTest, LargeCustomHiDpiSvgCursorIsRejected) {
diff --git a/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html b/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html
index d0172f2e..b00d7de 100644
--- a/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html
+++ b/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html
@@ -99,7 +99,7 @@
debug('TEST CASE: ' + node.textContent.trim());
// Make sure the node is visible and move the mouse over top of it.
document.documentElement.scrollTop = node.offsetTop - 50;
- eventSender.mouseMoveTo(node.offsetLeft + 3, node.offsetTop - document.documentElement.scrollTop + 3);
+ eventSender.mouseMoveTo(node.offsetLeft + 50, node.offsetTop - document.documentElement.scrollTop + 3);
// Get details of the current mouse cursor.
// Note that we could return structured data which we then validate, but that's a lot more
diff --git a/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html b/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html
index 4d48aae..da9cb53 100644
--- a/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html
+++ b/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html
@@ -29,7 +29,7 @@
// Make sure the node is visible and move the mouse over top of it.
document.documentElement.scrollTop = node.offsetTop - 50;
- eventSender.mouseMoveTo(node.offsetLeft + 3, node.offsetTop - document.documentElement.scrollTop + 3);
+ eventSender.mouseMoveTo(node.offsetLeft + 50, node.offsetTop - document.documentElement.scrollTop + 3);
// Get details of the current mouse cursor.
// Note that we could return structured data which we then validate, but that's a lot more
diff --git a/third_party/blink/web_tests/fast/events/mouse-cursor.html b/third_party/blink/web_tests/fast/events/mouse-cursor.html
index 4a5560f1..32d8c0e0 100644
--- a/third_party/blink/web_tests/fast/events/mouse-cursor.html
+++ b/third_party/blink/web_tests/fast/events/mouse-cursor.html
@@ -59,7 +59,7 @@
// Make sure the node is visible and move the mouse over top of it.
document.documentElement.scrollTop = node.offsetTop - 50;
- eventSender.mouseMoveTo(node.offsetLeft + 3, node.offsetTop - document.documentElement.scrollTop + 3);
+ eventSender.mouseMoveTo(node.offsetLeft + 50, node.offsetTop - document.documentElement.scrollTop + 3);
// Get details of the current mouse cursor.
// Note that we could return structured data which we then validate, but that's a lot more
Regression Test / PoC
diff --git a/content/browser/site_per_process_hit_test_browsertest.cc b/content/browser/site_per_process_hit_test_browsertest.cc
index 70463c3..478a015 100644
--- a/content/browser/site_per_process_hit_test_browsertest.cc
+++ b/content/browser/site_per_process_hit_test_browsertest.cc
@@ -4513,6 +4513,81 @@
}
}
+// Ensure small custom cursors are not allowed to intersect the root visual
+// viewport.
+IN_PROC_BROWSER_TEST_F(SitePerProcessHitTestBrowserTest,
+ SmallCursorRemovedInOffsetOOPIF) {
+ GURL url(R"(data:text/html,
+ <iframe id='iframe'
+ style ='position:absolute; top: -10px'
+ width=1000px height=1000px>
+ </iframe>)");
+ EXPECT_TRUE(NavigateToURL(shell(), url));
+
+ // The small-cursor.html document has a custom cursor that is 32x32 with a
+ // hotspot on the bottom right corner.
+ NavigateIframeToURL(web_contents(), "iframe",
+ embedded_test_server()->GetURL("/small-cursor.html"));
+
+ FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
+
+ FrameTreeNode* child_node = root->child_at(0);
+ EXPECT_NE(web_contents()->GetSiteInstance(),
+ child_node->current_frame_host()->GetSiteInstance());
+
+ WaitForHitTestData(child_node->current_frame_host());
+
+ RenderWidgetHostViewBase* root_view = static_cast<RenderWidgetHostViewBase*>(
+ root->current_frame_host()->GetRenderWidgetHost()->GetView());
+ RenderWidgetHostImpl* rwh_child =
+ root->child_at(0)->current_frame_host()->GetRenderWidgetHost();
+ RenderWidgetHostViewBase* child_view =
+ static_cast<RenderWidgetHostViewBase*>(rwh_child->GetView());
+
+ auto* router = web_contents()->GetInputEventRouter();
+ RenderWidgetHostMouseEventMonitor child_monitor(
+ child_view->GetRenderWidgetHost());
+ RenderWidgetHostMouseEventMonitor root_monitor(
+ root_view->GetRenderWidgetHost());
+
+ // A cursor with enough room in the root view to fully display without
+ // intersecting native UI should be shown.
+ {
+ blink::WebMouseEvent mouse_event(
+ blink::WebInputEvent::Type::kMouseMove,
+ blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ SetWebEventPositions(&mouse_event, gfx::Point(300, 300), root_view);
+ auto set_cursor_interceptor =
+ std::make_unique<SetCursorInterceptor>(rwh_child);
+ RouteMouseEventAndWaitUntilDispatch(router, root_view, child_view,
+ &mouse_event);
+ set_cursor_interceptor->Wait();
+ EXPECT_TRUE(set_cursor_interceptor->cursor().has_value());
+ EXPECT_EQ(32, set_cursor_interceptor->cursor()->custom_bitmap().width());
+ EXPECT_EQ(32, set_cursor_interceptor->cursor()->custom_bitmap().height());
+ }
+ // A cursor without enough room to be fully enclosed within the root view
+ // should not be shown, even if the iframe is at an offset. The default cursor
+ // should be shown instead.
+ {
+ blink::WebMouseEvent mouse_event(
+ blink::WebInputEvent::Type::kMouseMove,
+ blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ SetWebEventPositions(&mouse_event, gfx::Point(300, 20), root_view);
+ auto set_cursor_interceptor =
+ std::make_unique<SetCursorInterceptor>(rwh_child);
+ RouteMouseEventAndWaitUntilDispatch(router, root_view, child_view,
+ &mouse_event);
+ // We should see a new cursor come in that replaces the custom one.
+ set_cursor_interceptor->Wait();
+ EXPECT_TRUE(set_cursor_interceptor->cursor().has_value());
+ EXPECT_EQ(ui::mojom::CursorType::kPointer,
+ set_cursor_interceptor->cursor()->type());
+ }
+}
+
// Regression test for https://crbug.com/1454515. An OOPIF
// scrolled away from the main document should not allow
// large cursors to intersect browser UI.
diff --git a/content/test/content_test_bundle_data.filelist b/content/test/content_test_bundle_data.filelist
index 5e909d7..a81c94c5 100644
--- a/content/test/content_test_bundle_data.filelist
+++ b/content/test/content_test_bundle_data.filelist
@@ -9466,6 +9466,7 @@
data/site_isolation/xml.octet-stream.mock-http-headers
data/site_isolation/xml.txt
data/site_per_process_main.html
+data/small-cursor.html
data/speech/web_speech_recognition.html
data/stale-script.js
data/stale-script.js.mock-http-headers
diff --git a/content/test/data/small-cursor.html b/content/test/data/small-cursor.html
new file mode 100644
index 0000000..dd63676
--- /dev/null
+++ b/content/test/data/small-cursor.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+body {
+ height: 1000px;
+ width: 1000px;
+ cursor: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32" fill="red"/></svg>') 32 32, auto;
+}
+</style>
diff --git a/third_party/blink/renderer/core/input/event_handler_test.cc b/third_party/blink/renderer/core/input/event_handler_test.cc
index f08d22a3..82c20c39 100644
--- a/third_party/blink/renderer/core/input/event_handler_test.cc
+++ b/third_party/blink/renderer/core/input/event_handler_test.cc
@@ -2544,7 +2544,7 @@
Compositor().BeginFrame();
- // Move the cursor so no part of it intersects the viewport.
+ // Move the cursor so no part of it intersects the viewport boundary.
{
WebMouseEvent mouse_move_event(
WebMouseEvent::Type::kMouseMove, gfx::PointF(25, 25),
@@ -2558,9 +2558,23 @@
EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
}
- // Now, move the cursor so that it intersects the visual viewport. The cursor
- // should not be removed because it is below
- // kMaximumCursorSizeWithoutFallback.
+ // Move the cursor exactly on the boundary so it is still contained within
+ // the visual viewport.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(24, 24),
+ gfx::PointF(24, 24), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
+ }
+
+ // Now, move the cursor so that it intersects the visual viewport boundary.
+ // The cursor should be removed and fall back to the default pointer.
{
WebMouseEvent mouse_move_event(
WebMouseEvent::Type::kMouseMove, gfx::PointF(23, 23),
@@ -2571,8 +2585,83 @@
const ui::Cursor& cursor =
GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kPointer, cursor.type());
+ }
+}
+
+TEST_F(EventHandlerSimTest, CustomCursor32x32IntersectsViewport) {
+ WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+ SimRequest request("https://example.com/test.html", "text/html");
+ SimSubresourceRequest cursor_request("https://example.com/32x32.svg",
+ "image/svg+xml");
+ LoadURL("https://example.com/test.html");
+ request.Complete(
+ R"HTML(
+ <!DOCTYPE html>
+ <style>
+ div {
+ width: 300px;
+ height: 100px;
+ cursor: url('32x32.svg') 32 32, auto;
+ }
+ </style>
+ <div>foo</div>
+ )HTML");
+
+ GetDocument().UpdateStyleAndLayoutTree();
+
+ cursor_request.Complete(R"SVG(
+ <svg xmlns="http://www.w3.org/2000/svg" width="32px" height="32px">
+ <rect width="32" height="32" fill="red"/>
+ </svg>
+ )SVG");
+
+ Compositor().BeginFrame();
+
+ // Move the cursor so it is fully contained within the visual viewport.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(32, 32),
+ gfx::PointF(32, 32), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
}
+
+ // Move the cursor so that its top-left corner is at (0, 0) in the visual
+ // viewport. Hotspot 32 32 is clamped to (31, 31), so at Point(31, 31),
+ // the cursor rect starts at (0, 0) and is fully contained.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(31, 31),
+ gfx::PointF(31, 31), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kCustom, cursor.type());
+ }
+
+ // Move the cursor so that it extends beyond the visual viewport boundary.
+ // The cursor should be removed and fall back to the default pointer.
+ {
+ WebMouseEvent mouse_move_event(
+ WebMouseEvent::Type::kMouseMove, gfx::PointF(30, 30),
+ gfx::PointF(30, 30), WebPointerProperties::Button::kNoButton, 0, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ GetDocument().GetFrame()->GetEventHandler().HandleMouseMoveEvent(
+ mouse_move_event, Vector<WebMouseEvent>(), Vector<WebMouseEvent>());
+
+ const ui::Cursor& cursor =
+ GetDocument().GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(ui::mojom::blink::CursorType::kPointer, cursor.type());
+ }
}
TEST_F(EventHandlerSimTest, LargeCustomHiDpiSvgCursorIsRejected) {
diff --git a/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html b/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html
index d0172f2e..b00d7de 100644
--- a/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html
+++ b/third_party/blink/web_tests/fast/events/mouse-cursor-image-set.html
@@ -99,7 +99,7 @@
debug('TEST CASE: ' + node.textContent.trim());
// Make sure the node is visible and move the mouse over top of it.
document.documentElement.scrollTop = node.offsetTop - 50;
- eventSender.mouseMoveTo(node.offsetLeft + 3, node.offsetTop - document.documentElement.scrollTop + 3);
+ eventSender.mouseMoveTo(node.offsetLeft + 50, node.offsetTop - document.documentElement.scrollTop + 3);
// Get details of the current mouse cursor.
// Note that we could return structured data which we then validate, but that's a lot more
diff --git a/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html b/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html
index 4d48aae..da9cb53 100644
--- a/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html
+++ b/third_party/blink/web_tests/fast/events/mouse-cursor-multiframecur.html
@@ -29,7 +29,7 @@
// Make sure the node is visible and move the mouse over top of it.
document.documentElement.scrollTop = node.offsetTop - 50;
- eventSender.mouseMoveTo(node.offsetLeft + 3, node.offsetTop - document.documentElement.scrollTop + 3);
+ eventSender.mouseMoveTo(node.offsetLeft + 50, node.offsetTop - document.documentElement.scrollTop + 3);
// Get details of the current mouse cursor.
// Note that we could return structured data which we then validate, but that's a lot more
diff --git a/third_party/blink/web_tests/fast/events/mouse-cursor.html b/third_party/blink/web_tests/fast/events/mouse-cursor.html
index 4a5560f1..32d8c0e0 100644
--- a/third_party/blink/web_tests/fast/events/mouse-cursor.html
+++ b/third_party/blink/web_tests/fast/events/mouse-cursor.html
@@ -59,7 +59,7 @@
// Make sure the node is visible and move the mouse over top of it.
document.documentElement.scrollTop = node.offsetTop - 50;
- eventSender.mouseMoveTo(node.offsetLeft + 3, node.offsetTop - document.documentElement.scrollTop + 3);
+ eventSender.mouseMoveTo(node.offsetLeft + 50, node.offsetTop - document.documentElement.scrollTop + 3);
// Get details of the current mouse cursor.
// Note that we could return structured data which we then validate, but that's a lot more
Original Bug Report
Security: Custom Cursor hijacking 32x32 on padlock
VULNERABILITY DETAILS
this vulnerabilty like https://bugs.chromium.org/p/chromium/issues/detail?id=1454515 but this case is not using iframe, only use custom cursor 32x32. it is possible to bypass the 32x32 custom cursor hijacking mitigation and render a 32x32 cursor over omni box.
VERSION
Chrome Version 121.0.6115.2 (Official Build) dev (64-bit)
Operating System: Windows 10
REPRODUCTION CASE
-
Hover over anywhere on the page to gain the fake cursor
-
Using the fake cursor, click on the padlock icon and see fake status
-
Download cursor.png, popout.png, cursor-page.html (credit to jameshfisher.github.io) and serve it on origin A
-
frame.html hosted on origin B and edit the iframe to point to origin A. (Note that must be different origins as I think it has something to do with “OOPIFs”)
CREDIT INFORMATION
Externally reported security bugs may appear in Chrome release notes. If
this bug is included, how would you like to be credited?
Reporter credit: Hafiizh (https://www.linkedin.com/in/hafiizh-7aa6bb31/)