Chrome · WebAppInstalls
CVE-2026-106285
Spoofing in WebAppInstalls
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/intent_picker_bubble_view.cc |
modified | |
WebContentschrome/browser/ui/views/intent_picker_bubble_view.h |
modified | |
Eventchrome/browser/ui/views/intent_picker_bubble_view.h |
modified | |
Checkboxchrome/browser/ui/views/intent_picker_bubble_view.h |
modified | |
Widgetchrome/browser/ui/views/intent_picker_bubble_view.h |
modified |
Files Changed
chrome/browser/ui/views/intent_picker_bubble_view_unittest.ccchrome/browser/ui/views/intent_picker_bubble_view.hchrome/browser/ui/views/intent_picker_bubble_view.cc
Patch
From 12db949bfb0039e8f194030e871905d2effc11cb Mon Sep 17 00:00:00 2001 From: Kunjan Vinay Patel <[email protected]> Date: Wed, 09 Sep 2026 19:26:17 -0700 Subject: [PATCH] Fix input protection bypass in IntentPickerBubbleView IntentPickerBubbleView allowed immediate acceptance of the dialog via double-clicks on app rows/grid items or VKEY_RETURN on grid items by calling AcceptDialog() directly. This bypassed DialogClientView's InputEventActivationProtector, leaving the dialog susceptible to rapid unintended interactions and clickjacking. This CL: - Threads the triggering ui::Event through the selection callbacks in both list and grid view. - Gates AcceptDialog() in OnAppSelected() behind DialogClientView::IsPossiblyUnintendedInteraction(). - Adds a unit test verifying that double-clicks within the initial protection cooldown window are ignored while clicks after the cooldown succeed. Bug: 533119681 Change-Id: I1750bc01dde3706895aa09c42338f8576b32dd7b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8375122 Commit-Queue: Kunjan Patel <[email protected]> Reviewed-by: Daniel Murphy <[email protected]> Reviewed-by: Darryl James <[email protected]> Cr-Commit-Position: refs/heads/main@{#1695114} --- diff --git a/chrome/browser/ui/views/intent_picker_bubble_view.cc b/chrome/browser/ui/views/intent_picker_bubble_view.cc index 342b447..43d0f6fa 100644 --- a/chrome/browser/ui/views/intent_picker_bubble_view.cc +++ b/chrome/browser/ui/views/intent_picker_bubble_view.cc @@ -60,6 +60,7 @@ #include "ui/views/style/typography_provider.h" #include "ui/views/view_class_properties.h" #include "ui/views/view_utils.h" +#include "ui/views/window/dialog_client_view.h" #if BUILDFLAG(IS_CHROMEOS) #include "ui/chromeos/devicetype_utils.h" @@ -97,8 +98,8 @@ // Callback for when an app is selected in the app list. First parameter is the // index, second parameter is true if the dialog should be immediately accepted. -using AppSelectedCallback = - base::RepeatingCallback<void(std::optional<size_t>, bool)>; +using AppSelectedCallback = base::RepeatingCallback< + void(std::optional<size_t>, bool, const ui::Event*)>; // Grid view: @@ -110,7 +111,8 @@ public: // Callback for when this app is selected. Parameter is true if the dialog // should be immediately accepted. - using ButtonSelectedCallback = base::RepeatingCallback<void(bool)>; + using ButtonSelectedCallback = + base::RepeatingCallback<void(bool, const ui::Event*)>; IntentPickerAppGridButton(ButtonSelectedCallback selected_callback, const ui::ImageModel& icon_model, @@ -185,7 +187,7 @@ void OnFocus() override { Button::OnFocus(); if (select_on_focus_) { - selected_callback_.Run(false); + selected_callback_.Run(false, nullptr); } } bool HandleAccessibleAction(const ui::AXActionData& action_data) override { @@ -217,7 +219,7 @@ bool should_open = IsDoubleClick(event) || (event.IsKeyEvent() && event.AsKeyEvent()->key_code() == ui::VKEY_RETURN); - selected_callback_.Run(should_open); + selected_callback_.Run(should_open, &event); } // Updates the accessible name of the bubble in the ViewsAX cache. @@ -300,7 +302,7 @@ } void SetSelectedIndex(std::optional<size_t> index) override { - SetSelectedIndexInternal(index, false); + SetSelectedIndexInternal(index, false, nullptr); } std::optional<size_t> GetSelectedIndex() const override { @@ -309,7 +311,8 @@ private: void SetSelectedIndexInternal(std::optional<size_t> new_index, - bool accepted) { + bool accepted, + const ui::Event* event) { if (selected_app_index_.has_value()) { GetButtonAtIndex(selected_app_index_.value())->SetSelected(false); } @@ -324,7 +327,7 @@ selected_app_index_ = new_index; - selected_callback_.Run(new_index, accepted); + selected_callback_.Run(new_index, accepted, event); } IntentPickerAppGridButton* GetButtonAtIndex(size_t index) { @@ -475,7 +478,7 @@ accepted = true; } - selected_callback_.Run(index, accepted); + selected_callback_.Run(index, accepted, event); } size_t CalculateNextAppIndex(int delta) { @@ -686,7 +689,8 @@ } void IntentPickerBubbleView::OnAppSelected(std::optional<size_t> index, - bool accepted) { + bool accepted, + const ui::Event* event) { SetButtonEnabled(ui::mojom::DialogButton::kOk, index.has_value()); if (index.has_value()) { @@ -695,6 +699,13 @@ if (accepted) { DCHECK(index.has_value()); + DCHECK(event); + if (GetDialogClientView() && + GetDialogClientView()->IsPossiblyUnintendedInteraction( + *event, + /*allow_key_events=*/ShouldAllowKeyEventsDuringInputProtection())) { + return; + } AcceptDialog(); } } diff --git a/chrome/browser/ui/views/intent_picker_bubble_view.h b/chrome/browser/ui/views/intent_picker_bubble_view.h index bc991eef..d9e635d 100644 --- a/chrome/browser/ui/views/intent_picker_bubble_view.h +++ b/chrome/browser/ui/views/intent_picker_bubble_view.h @@ -26,6 +26,10 @@ class WebContents; } // namespace content +namespace ui { +class Event; +} // namespace ui + namespace views { class Checkbox; class Widget; @@ -141,7 +145,9 @@ // |accepted| is true, the dialog should be immediately accepted with that app // selected. If |index| is nullopt, no app is selected, and the Accept button // will be disabled - void OnAppSelected(std::optional<size_t> index, bool accepted); + void OnAppSelected(std::optional<size_t> index, + bool accepted, + const ui::Event* event); void Initialize(); diff --git a/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc b/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc index 8859af0e..1b83d34 100644 --- a/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc +++ b/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc @@ -50,6 +50,7 @@ #include "ui/views/test/views_test_base.h" #include "ui/views/view_utils.h" #include "ui/views/widget/widget_utils.h" +#include "ui/views/window/dialog_client_view.h" #include "url/gurl.h" #include "url/origin.h" @@ -539,6 +540,9 @@ BubbleType::kLinkCapturing, /*initiating_origin=*/std::nullopt); + // Bypass input protection cooldown. + bubble()->GetDialogClientView()->ResetViewShownTimeStampForTesting(); + views::test::ButtonTestApi button(GetButtonAtIndex(0)); button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(), @@ -552,6 +556,42 @@ EXPECT_EQ(last_close_reason(), apps::IntentPickerCloseReason::OPEN_APP); } +TEST_P(IntentPickerBubbleViewLayoutTest, + DoubleClickBlockedDuringInputProtection) { + AddApp(apps::PickerEntryType::kWeb, "web_app_id", "Web App"); + auto bubble_widget = + CreateBubbleView(/*use_icons=*/false, /*show_stay_in_chrome=*/false, + BubbleType::kLinkCapturing, + /*initiating_origin=*/std::nullopt); + + views::test::ButtonTestApi button(GetButtonAtIndex(0)); + + // Double-click immediately upon bubble appearance (within protection window). + button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(), + gfx::PointF(), ui::EventTimeForNow(), + ui::EF_NONE, ui::EF_NONE)); + button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(), + gfx::PointF(), ui::EventTimeForNow(), + ui::EF_IS_DOUBLE_CLICK, ui::EF_NONE)); + + EXPECT_FALSE(bubble_widget->IsClosed()); + EXPECT_EQ(bubble()->GetSelectedIndex(), 0u); + EXPECT_TRUE(last_selected_launch_name().empty()); + + // Reset cooldown and verify double-click now succeeds. + bubble()->GetDialogClientView()->ResetViewShownTimeStampForTesting(); + + button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(), + gfx::PointF(), ui::EventTimeForNow(), + ui::EF_NONE, ui::EF_NONE)); + button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(), + gfx::PointF(), ui::EventTimeForNow(), + ui::EF_IS_DOUBLE_CLICK, ui::EF_NONE)); + + EXPECT_EQ(last_selected_launch_name(), "web_app_id"); + EXPECT_EQ(last_close_reason(), apps::IntentPickerCloseReason::OPEN_APP); +} + INSTANTIATE_TEST_SUITE_P( All, IntentPickerBubbleViewLayoutTest,
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc b/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc
index 8859af0e..1b83d34 100644
--- a/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc
+++ b/chrome/browser/ui/views/intent_picker_bubble_view_unittest.cc
@@ -50,6 +50,7 @@
#include "ui/views/test/views_test_base.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget_utils.h"
+#include "ui/views/window/dialog_client_view.h"
#include "url/gurl.h"
#include "url/origin.h"
@@ -539,6 +540,9 @@
BubbleType::kLinkCapturing,
/*initiating_origin=*/std::nullopt);
+ // Bypass input protection cooldown.
+ bubble()->GetDialogClientView()->ResetViewShownTimeStampForTesting();
+
views::test::ButtonTestApi button(GetButtonAtIndex(0));
button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(),
@@ -552,6 +556,42 @@
EXPECT_EQ(last_close_reason(), apps::IntentPickerCloseReason::OPEN_APP);
}
+TEST_P(IntentPickerBubbleViewLayoutTest,
+ DoubleClickBlockedDuringInputProtection) {
+ AddApp(apps::PickerEntryType::kWeb, "web_app_id", "Web App");
+ auto bubble_widget =
+ CreateBubbleView(/*use_icons=*/false, /*show_stay_in_chrome=*/false,
+ BubbleType::kLinkCapturing,
+ /*initiating_origin=*/std::nullopt);
+
+ views::test::ButtonTestApi button(GetButtonAtIndex(0));
+
+ // Double-click immediately upon bubble appearance (within protection window).
+ button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(),
+ gfx::PointF(), ui::EventTimeForNow(),
+ ui::EF_NONE, ui::EF_NONE));
+ button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(),
+ gfx::PointF(), ui::EventTimeForNow(),
+ ui::EF_IS_DOUBLE_CLICK, ui::EF_NONE));
+
+ EXPECT_FALSE(bubble_widget->IsClosed());
+ EXPECT_EQ(bubble()->GetSelectedIndex(), 0u);
+ EXPECT_TRUE(last_selected_launch_name().empty());
+
+ // Reset cooldown and verify double-click now succeeds.
+ bubble()->GetDialogClientView()->ResetViewShownTimeStampForTesting();
+
+ button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(),
+ gfx::PointF(), ui::EventTimeForNow(),
+ ui::EF_NONE, ui::EF_NONE));
+ button.NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(),
+ gfx::PointF(), ui::EventTimeForNow(),
+ ui::EF_IS_DOUBLE_CLICK, ui::EF_NONE));
+
+ EXPECT_EQ(last_selected_launch_name(), "web_app_id");
+ EXPECT_EQ(last_close_reason(), apps::IntentPickerCloseReason::OPEN_APP);
+}
+
INSTANTIATE_TEST_SUITE_P(
All,
IntentPickerBubbleViewLayoutTest,
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page