Low watch CVSS 6.5 chrome Spoofing 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Web Authentication (Passkeys & Security Keys)
DescriptionUI misrepresentation in Web Authentication (Passkeys & Security Keys)
ComponentChromium
Sub-componentWebAuthn
Bug ClassSpoofing
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.
Tracker522304549
Fix commit9d7d8afd1cb8 (chromium/src) +124/-2
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-10 (per vendor advisory)
Fix landed2026-07-06 (public git)
Disclosed2026-08-25
Patch gap50 days — fix public in source before the advisory

Background

`HoverListView`
the Views control that renders each selectable row (mechanism/passkey/security-key option) in the WebAuthn tab-modal dialog.
`InputEventActivationProtector` / `IsPossiblyUnintendedInteraction`
a Views mechanism (exposed via DialogClientView) that drops clicks arriving within a short window (500 ms) after a dialog appears, defending against clickjacking/double-click hijacking.
`DialogClientView`
the client area of a dialog that owns the input-protection state and the shown-time timestamp used to judge whether an interaction is likely unintended.
Tapjacking/input protection interval
the interval right after a dialog is presented during which a genuine user is unlikely to have deliberately clicked, so clicks are treated as possibly spoofed.

Root Cause Analysis

The WebAuthn dialog’s built-in accept/cancel buttons go through DialogClientView, which applies InputEventActivationProtector to reject clicks landing in the 500 ms post-presentation window. But the per-row buttons created by HoverListView bound their click callback directly to HoverListModel::OnListItemSelected via base::BindRepeating(..., base::Unretained(model_.get()), item_tag), bypassing the dialog’s protector entirely. As a result a row click occurring immediately upon dialog presentation was executed rather than dropped, so a page that could get a click delivered as the dialog appeared could cause selection of a WebAuthn mechanism without a deliberate user action.

The fix reroutes the row callback to a new HoverListView::OnListItemSelected(item_tag, event), which walks GetWidget()->widget_delegate()->AsDialogDelegate()->GetDialogClientView() and calls IsPossiblyUnintendedInteraction(event, /*allow_key_events=*/false), returning early (dropping the selection) when the interaction is within the protection window, and only otherwise forwarding to model_->OnListItemSelected(item_tag).

Key insight
The single mistake was wiring row-button clicks straight to the model, so they escaped the dialog’s InputEventActivationProtector; the fix interposes HoverListView::OnListItemSelected, which consults IsPossiblyUnintendedInteraction before dispatching the selection.

Attack Path

  1. Present the WebAuthn dialog A page initiates a Web Authentication request, causing the tab-modal authenticator dialog with its HoverListView rows to appear.
  2. Deliver a click into the protection window Using timing or overlay/clickjacking techniques (inferred), the attacker arranges for a click to land on the top row button within the 500 ms input-protection interval as the dialog is shown.
  3. Bypass the protector Because the row callback was bound directly to HoverListModel::OnListItemSelected, the click was not routed through DialogClientView’s protector and was executed (the regression test shows the pre-fix callback firing immediately).
  4. Unintended mechanism selection The victim’s authenticator mechanism (e.g. a transport/security key) is selected without a deliberate user choice, a UI-misrepresentation/interaction-hijack against the WebAuthn flow.

Impact Assessment

An attacker can cause a WebAuthn dialog row to be selected via a click the user did not intend, in the browser UI context, defeating the input protection meant to guard the authentication ceremony. Preconditions are the ability to trigger the WebAuthn dialog and to land a click within the 500 ms window (the broader clickjacking setup is inferred). The advisory classifies this as UI misrepresentation (spoofing) in Web Authentication; no exploited-in-the-wild status is stated, and no memory-safety or escalation beyond the interaction bypass is demonstrated by the patch.

Changed Functions

FunctionChangeNotes
Event
chrome/browser/ui/views/webauthn/hover_list_view.h
modified
Separator
chrome/browser/ui/views/webauthn/hover_list_view.h
modified
if
chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
modified
StepTransitionObserver
chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
modified
BindLambdaForTesting
chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/views/webauthn/hover_list_view.h
  • chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
  • chrome/browser/ui/views/webauthn/BUILD.gn
  • chrome/browser/ui/views/webauthn/hover_list_view.cc

Audit Directions

  • Custom row/list buttons in dialogs
    Audit dialog subviews that create their own buttons and bind callbacks directly to models/handlers; verify they still pass through InputEventActivationProtector/IsPossiblyUnintendedInteraction.
  • Security-sensitive dialogs
    Review other permission/authentication/consent dialogs (WebAuthn, payments, permissions) for interactive elements that bypass the dialog’s default input protection.
  • Callback binding shortcuts
    Flag base::BindRepeating(..., base::Unretained(model)) on UI controls where the click should first be screened by the owning view/dialog for unintended-interaction protection.
From 9d7d8afd1cb8ef089b616691c7a579996ef1bd2b Mon Sep 17 00:00:00 2001
From: Adem Derinel <derinel@google.com>
Date: Mon, 06 Jul 2026 08:47:38 -0700
Subject: [PATCH] [WebAuthn] Prevent input protection bypass on HoverListView row clicks

The WebAuthn tab-modal dialog HoverListView row buttons were not
protected by DialogClientView InputEventActivationProtector. As a
result, row clicks occurring immediately upon dialog presentation during
the double-click/input protection interval (500 ms) were executed
instead of dropped.

This change intercepts row button selections across HoverListView
(OnListItemSelected), querying
DialogClientView::IsPossiblyUnintendedInteraction(event, false) before
dispatching item selection callbacks
(HoverListModel::OnListItemSelected) or accepting actions.

Added regression browser test
(AuthenticatorDialogViewTest.InputEventProtection) to verify row clicks
inside the protection window are dropped.

TAG=agy

Fixed: 522304549
Change-Id: Ia4d54fce53a04da71b0095b988c4588f046c9c7a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8039609
Reviewed-by: Nina Satragno <nsatragno@chromium.org>
Commit-Queue: Adem Derinel <derinel@google.com>
Cr-Commit-Position: refs/heads/main@{#1657224}
---
diff --git a/chrome/browser/ui/views/webauthn/hover_list_view.cc b/chrome/browser/ui/views/webauthn/hover_list_view.cc
index 3f4e07a..f9e224b 100644
--- a/chrome/browser/ui/views/webauthn/hover_list_view.cc
+++ b/chrome/browser/ui/views/webauthn/hover_list_view.cc
@@ -19,6 +19,7 @@
 #include "ui/base/models/image_model.h"
 #include "ui/base/ui_base_features.h"
 #include "ui/color/color_id.h"
+#include "ui/events/event.h"
 #include "ui/gfx/geometry/insets.h"
 #include "ui/views/border.h"
 #include "ui/views/controls/button/button.h"
@@ -26,6 +27,10 @@
 #include "ui/views/controls/separator.h"
 #include "ui/views/layout/box_layout.h"
 #include "ui/views/layout/fill_layout.h"
+#include "ui/views/widget/widget.h"
+#include "ui/views/widget/widget_delegate.h"
+#include "ui/views/window/dialog_client_view.h"
+#include "ui/views/window/dialog_delegate.h"
 
 namespace {
 
@@ -89,10 +94,12 @@
                                        std::u16string description_text,
                                        bool enabled,
                                        int item_tag) {
+  // Safe because the hover button is added as a child view owned by `this`,
+  // guaranteeing `this` strictly outlives `hover_button` and its callback.
   auto hover_button = CreateHoverButtonForListItem(
       icon, item_text, description_text, enabled,
-      base::BindRepeating(&HoverListModel::OnListItemSelected,
-                          base::Unretained(model_.get()), item_tag));
+      base::BindRepeating(&HoverListView::OnListItemSelected,
+                          base::Unretained(this), item_tag));
 
   auto* list_item_view_ptr = hover_button.release();
   item_container_->AddChildViewRaw(list_item_view_ptr);
@@ -102,6 +109,22 @@
       item_tag, ListItemViews{list_item_view_ptr, separator});
 }
 
+void HoverListView::OnListItemSelected(int item_tag, const ui::Event& event) {
+  if (GetWidget() && GetWidget()->widget_delegate()) {
+    if (views::DialogDelegate* dialog_delegate =
+            GetWidget()->widget_delegate()->AsDialogDelegate()) {
+      if (views::DialogClientView* dialog_client_view =
+              dialog_delegate->GetDialogClientView()) {
+        if (dialog_client_view->IsPossiblyUnintendedInteraction(
+                event, /*allow_key_events=*/false)) {
+          return;
+        }
+      }
+    }
+  }
+  model_->OnListItemSelected(item_tag);
+}
+
 views::Button& HoverListView::GetTopListItemView() const {
   DCHECK(!tags_to_list_item_views_.empty());
   return *tags_to_list_item_views_.begin()->second.item_view;
diff --git a/chrome/browser/ui/views/webauthn/hover_list_view.h b/chrome/browser/ui/views/webauthn/hover_list_view.h
index b51535ad..8fa378f 100644
--- a/chrome/browser/ui/views/webauthn/hover_list_view.h
+++ b/chrome/browser/ui/views/webauthn/hover_list_view.h
@@ -17,6 +17,10 @@
 #include "ui/views/controls/scroll_view.h"
 #include "ui/views/view.h"
 
+namespace ui {
+class Event;
+}  // namespace ui
+
 namespace views {
 class Separator;
 }  // namespace views
@@ -57,6 +61,7 @@
                           std::u16string item_description,
                           bool enabled,
                           int item_tag);
+  void OnListItemSelected(int item_tag, const ui::Event& event);
   views::Button& GetTopListItemView() const;
   int GetPreferredViewHeight() const;
 
diff --git a/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc b/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
index b8e0252..092bb168 100644
--- a/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
+++ b/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
@@ -8,7 +8,9 @@
 
 #include "base/functional/callback_helpers.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/test/bind.h"
 #include "build/build_config.h"
+#include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -17,11 +19,14 @@
 #include "chrome/browser/ui/views/webauthn/authenticator_request_dialog_view_controller_views.h"
 #include "chrome/browser/ui/views/webauthn/authenticator_request_dialog_view_test_api.h"
 #include "chrome/browser/ui/views/webauthn/authenticator_request_sheet_view.h"
+#include "chrome/browser/ui/views/webauthn/hover_list_view.h"
 #include "chrome/browser/ui/webauthn/authenticator_request_sheet_model.h"
 #include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
 #include "content/public/test/browser_test.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/views/controls/label.h"
+#include "ui/views/test/button_test_api.h"
+#include "ui/views/window/dialog_client_view.h"
 
 namespace {
 
@@ -92,6 +97,36 @@
   }
 };
 
+HoverListView* FindHoverListView(views::View* root) {
+  if (!root) {
+    return nullptr;
+  }
+  if (auto* hover_list = views::AsViewClass<HoverListView>(root)) {
+    return hover_list;
+  }
+  for (views::View* child : root->children()) {
+    if (HoverListView* found = FindHoverListView(child)) {
+      return found;
+    }
+  }
+  return nullptr;
+}
+
+views::Button* FindTopListItemButton(views::View* root) {
+  if (!root) {
+    return nullptr;
+  }
+  if (auto* button = views::AsViewClass<views::Button>(root)) {
+    return button;
+  }
+  for (views::View* child : root->children()) {
+    if (views::Button* found = FindTopListItemButton(child)) {
+      return found;
+    }
+  }
+  return nullptr;
+}
+
 }  // namespace
 
 class StepTransitionObserver
@@ -163,3 +198,61 @@
 IN_PROC_BROWSER_TEST_F(AuthenticatorDialogViewTest, InvokeUi_ReplaceSheet) {
   ShowAndVerifyUi();
 }
+
+IN_PROC_BROWSER_TEST_F(AuthenticatorDialogViewTest,
+                       HoverListViewInputEventProtection) {
+  content::WebContents* const web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  CHECK(web_contents);
+
+  int mechanism_callback_count_ = 0;
+  auto dialog_model =
+      base::MakeRefCounted<AuthenticatorRequestDialogModel>(nullptr);
+  dialog_model->relying_party_id = "example.com";
+  dialog_model->mechanisms.emplace_back(
+      AuthenticatorRequestDialogModel::Mechanism::Transport(
+          AuthenticatorTransport::kUsbHumanInterfaceDevice),
+      u"Security key", kPasskeyUsbDarkCustomIcon,
+      base::BindRepeating(
+          base::BindLambdaForTesting([&]() { mechanism_callback_count_++; })));
+  dialog_model->SetStep(
+      AuthenticatorRequestDialogModel::Step::kMechanismSelection);
+
+  auto view_controller =
+      std::make_unique<AuthenticatorRequestDialogViewControllerViews>(
+          web_contents, dialog_model.get());
+
+  // Trigger OnStepTransition() so that view_controller creates the sheet for
+  // the current step and calls Show().
+  view_controller->OnStepTransition();
+
+  AuthenticatorRequestSheetView* sheet =
+      test::AuthenticatorRequestDialogViewTestApi::GetSheet(
+          view_controller.get());
+  ASSERT_TRUE(sheet);
+
+  HoverListView* hover_list_view = FindHoverListView(sheet);
+  ASSERT_TRUE(hover_list_view);
+
+  views::Button* top_row_button = FindTopListItemButton(hover_list_view);
+  ASSERT_TRUE(top_row_button);
+
+  ui::MouseEvent click_event(ui::EventType::kMousePressed, gfx::Point(),
+                             gfx::Point(), base::TimeTicks::Now(),
+                             ui::EF_LEFT_MOUSE_BUTTON,
+                             ui::EF_LEFT_MOUSE_BUTTON);
+
+  views::test::ButtonTestApi(top_row_button).NotifyClick(click_event);
+  EXPECT_EQ(mechanism_callback_count_, 0);
+
+  ASSERT_TRUE(sheet->GetWidget());
+  auto* dialog_delegate =
+      sheet->GetWidget()->widget_delegate()->AsDialogDelegate();
+  ASSERT_TRUE(dialog_delegate);
+  auto* dialog_client_view = dialog_delegate->GetDialogClientView();
+  ASSERT_TRUE(dialog_client_view);
+
+  dialog_client_view->ResetViewShownTimeStampForTesting();
+  views::test::ButtonTestApi(top_row_button).NotifyClick(click_event);
+  EXPECT_EQ(mechanism_callback_count_, 1);
+}
diff --git a/chrome/browser/ui/views/webauthn/BUILD.gn b/chrome/browser/ui/views/webauthn/BUILD.gn
index fcb6201..08b0655 100644
--- a/chrome/browser/ui/views/webauthn/BUILD.gn
+++ b/chrome/browser/ui/views/webauthn/BUILD.gn
@@ -201,5 +201,6 @@
     "//content/test:test_support",
     "//testing/gtest",
     "//ui/views",
+    "//ui/views:test_support",
   ]
 }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc b/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
index b8e0252..092bb168 100644
--- a/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
+++ b/chrome/browser/ui/views/webauthn/authenticator_dialog_view_browsertest.cc
@@ -8,7 +8,9 @@
 
 #include "base/functional/callback_helpers.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/test/bind.h"
 #include "build/build_config.h"
+#include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -17,11 +19,14 @@
 #include "chrome/browser/ui/views/webauthn/authenticator_request_dialog_view_controller_views.h"
 #include "chrome/browser/ui/views/webauthn/authenticator_request_dialog_view_test_api.h"
 #include "chrome/browser/ui/views/webauthn/authenticator_request_sheet_view.h"
+#include "chrome/browser/ui/views/webauthn/hover_list_view.h"
 #include "chrome/browser/ui/webauthn/authenticator_request_sheet_model.h"
 #include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
 #include "content/public/test/browser_test.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/views/controls/label.h"
+#include "ui/views/test/button_test_api.h"
+#include "ui/views/window/dialog_client_view.h"
 
 namespace {
 
@@ -92,6 +97,36 @@
   }
 };
 
+HoverListView* FindHoverListView(views::View* root) {
+  if (!root) {
+    return nullptr;
+  }
+  if (auto* hover_list = views::AsViewClass<HoverListView>(root)) {
+    return hover_list;
+  }
+  for (views::View* child : root->children()) {
+    if (HoverListView* found = FindHoverListView(child)) {
+      return found;
+    }
+  }
+  return nullptr;
+}
+
+views::Button* FindTopListItemButton(views::View* root) {
+  if (!root) {
+    return nullptr;
+  }
+  if (auto* button = views::AsViewClass<views::Button>(root)) {
+    return button;
+  }
+  for (views::View* child : root->children()) {
+    if (views::Button* found = FindTopListItemButton(child)) {
+      return found;
+    }
+  }
+  return nullptr;
+}
+
 }  // namespace
 
 class StepTransitionObserver
@@ -163,3 +198,61 @@
 IN_PROC_BROWSER_TEST_F(AuthenticatorDialogViewTest, InvokeUi_ReplaceSheet) {
   ShowAndVerifyUi();
 }
+
+IN_PROC_BROWSER_TEST_F(AuthenticatorDialogViewTest,
+                       HoverListViewInputEventProtection) {
+  content::WebContents* const web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  CHECK(web_contents);
+
+  int mechanism_callback_count_ = 0;
+  auto dialog_model =
+      base::MakeRefCounted<AuthenticatorRequestDialogModel>(nullptr);
+  dialog_model->relying_party_id = "example.com";
+  dialog_model->mechanisms.emplace_back(
+      AuthenticatorRequestDialogModel::Mechanism::Transport(
+          AuthenticatorTransport::kUsbHumanInterfaceDevice),
+      u"Security key", kPasskeyUsbDarkCustomIcon,
+      base::BindRepeating(
+          base::BindLambdaForTesting([&]() { mechanism_callback_count_++; })));
+  dialog_model->SetStep(
+      AuthenticatorRequestDialogModel::Step::kMechanismSelection);
+
+  auto view_controller =
+      std::make_unique<AuthenticatorRequestDialogViewControllerViews>(
+          web_contents, dialog_model.get());
+
+  // Trigger OnStepTransition() so that view_controller creates the sheet for
+  // the current step and calls Show().
+  view_controller->OnStepTransition();
+
+  AuthenticatorRequestSheetView* sheet =
+      test::AuthenticatorRequestDialogViewTestApi::GetSheet(
+          view_controller.get());
+  ASSERT_TRUE(sheet);
+
+  HoverListView* hover_list_view = FindHoverListView(sheet);
+  ASSERT_TRUE(hover_list_view);
+
+  views::Button* top_row_button = FindTopListItemButton(hover_list_view);
+  ASSERT_TRUE(top_row_button);
+
+  ui::MouseEvent click_event(ui::EventType::kMousePressed, gfx::Point(),
+                             gfx::Point(), base::TimeTicks::Now(),
+                             ui::EF_LEFT_MOUSE_BUTTON,
+                             ui::EF_LEFT_MOUSE_BUTTON);
+
+  views::test::ButtonTestApi(top_row_button).NotifyClick(click_event);
+  EXPECT_EQ(mechanism_callback_count_, 0);
+
+  ASSERT_TRUE(sheet->GetWidget());
+  auto* dialog_delegate =
+      sheet->GetWidget()->widget_delegate()->AsDialogDelegate();
+  ASSERT_TRUE(dialog_delegate);
+  auto* dialog_client_view = dialog_delegate->GetDialogClientView();
+  ASSERT_TRUE(dialog_client_view);
+
+  dialog_client_view->ResetViewShownTimeStampForTesting();
+  views::test::ButtonTestApi(top_row_button).NotifyClick(click_event);
+  EXPECT_EQ(mechanism_callback_count_, 1);
+}
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.