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 <ksukh@google.com>
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 <ksukh@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Darryl James <dljames@chromium.org>
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