Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Passwords
DescriptionIncorrect security UI in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker501685207
Fix commitf31b8a550d6e (chromium/src) +95/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.cc
modified
Event
chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.h
modified
PasswordCrossDomainConfirmationPopupViewViews
chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.h
modified

Files Changed

  • chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.cc
  • chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.h
  • chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc
From f31b8a550d6edea5f8c5b93ce79d63cb10eef436 Mon Sep 17 00:00:00 2001
From: Talita Halboth <talitahalboth@google.com>
Date: Tue, 21 Apr 2026 07:41:51 -0700
Subject: [PATCH] Protect cross-domain password confirmation popup from keyjacking

This CL adds an InputEventActivationProtector to the
PasswordCrossDomainConfirmationPopupViewViews to mitigate keyjacking
attacks. It prevents unintended confirmations by ignoring inputs that
occur immediately after the popup is shown.

Additionally, the initially focused view is changed from the confirm
button to the cancel button to provide a safer default behavior.

Tests are added to ensure the protector works as expected.

Bug: b:501685207
Change-Id: If3d54721b7880545847bcca2392553ab7b3bcf3e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7777857
Reviewed-by: Oleksandr Tara <otara@google.com>
Commit-Queue: Talita Halboth <talitahalboth@google.com>
Reviewed-by: Mohamed Amir Yosef <mamir@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618189}
---

diff --git a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.cc b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.cc
index bac6abd0..33c9669 100644
--- a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.cc
+++ b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.cc
@@ -22,6 +22,7 @@
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/base/ui_base_types.h"
+#include "ui/events/event.h"
 #include "ui/gfx/geometry/insets.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/gfx/text_constants.h"
@@ -50,7 +51,8 @@
         base::OnceClosure cancel_callback)
     : autofill::PopupBaseView(controller,
                               parent_widget,
-                              views::Widget::InitParams::Activatable::kYes) {
+                              views::Widget::InitParams::Activatable::kYes),
+      confirmation_callback_(std::move(confirmation_callback)) {
   SetBackground(views::CreateSolidBackground(ui::kColorDropdownBackground));
 
   auto* layout_provider = ChromeLayoutProvider::Get();
@@ -105,17 +107,22 @@
           .SetBetweenChildSpacing(layout_provider->GetDistanceMetric(
               views::DISTANCE_RELATED_BUTTON_HORIZONTAL))
           .Build());
-  controls->AddChildView(views::Builder<views::MdTextButton>()
-                             .SetText(l10n_util::GetStringUTF16(IDS_CANCEL))
-                             .SetStyle(ui::ButtonStyle::kDefault)
-                             .SetCallback(std::move(cancel_callback))
-                             .Build());
+  auto* cancel_button = controls->AddChildView(
+      views::Builder<views::MdTextButton>()
+          .SetText(l10n_util::GetStringUTF16(IDS_CANCEL))
+          .SetStyle(ui::ButtonStyle::kDefault)
+          .SetCallback(std::move(cancel_callback))
+          .SetID(static_cast<int>(PopupViewId::kCancelButton))
+          .Build());
   auto* confirmation_button = controls->AddChildView(
       views::Builder<views::MdTextButton>()
           .SetText(l10n_util::GetStringUTF16(
               IDS_PASSWORD_CROSS_DOMAIN_FILLING_CONFIRMATION_CONFIRM_BUTTON_LABEL))
           .SetStyle(ui::ButtonStyle::kProminent)
-          .SetCallback(std::move(confirmation_callback))
+          .SetCallback(base::BindRepeating(
+              &PasswordCrossDomainConfirmationPopupViewViews::OnConfirm,
+              base::Unretained(this)))
+          .SetID(static_cast<int>(PopupViewId::kConfirmButton))
           .Build());
   confirmation_button->GetViewAccessibility().SetName(base::JoinString(
       {controller->GetTitleText(), controller->GetBodyText(),
@@ -126,7 +133,18 @@
                              layout_provider->GetDistanceMetric(
                                  DISTANCE_STANDALONE_BUBBLE_PREFERRED_WIDTH));
   SetPreferredSize(gfx::Size(popup_width, GetHeightForWidth(popup_width)));
-  SetInitiallyFocusedView(confirmation_button);
+  SetInitiallyFocusedView(cancel_button);
+}
+
+void PasswordCrossDomainConfirmationPopupViewViews::OnConfirm(
+    const ui::Event& event) {
+  if (input_protector_.IsPossiblyUnintendedInteraction(
+          event, /*allow_key_events=*/false)) {
+    return;
+  }
+  if (confirmation_callback_) {
+    std::move(confirmation_callback_).Run();
+  }
 }
 
 PasswordCrossDomainConfirmationPopupViewViews::
@@ -147,6 +165,7 @@
 
 void PasswordCrossDomainConfirmationPopupViewViews::Show() {
   DoShow();
+  input_protector_.VisibilityChanged(true);
 }
 
 BEGIN_METADATA(PasswordCrossDomainConfirmationPopupViewViews)
diff --git a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.h b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.h
index b2968750..bee87d3 100644
--- a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.h
+++ b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.h
@@ -15,6 +15,11 @@
 #include "chrome/browser/ui/passwords/password_cross_domain_confirmation_popup_view.h"
 #include "chrome/browser/ui/views/autofill/popup/popup_base_view.h"
 #include "ui/base/metadata/metadata_header_macros.h"
+#include "ui/views/input_event_activation_protector.h"
+
+namespace ui {
+class Event;
+}
 
 // The views implementation of the `PasswordCrossDomainConfirmationPopupView`.
 class PasswordCrossDomainConfirmationPopupViewViews
@@ -24,6 +29,12 @@
                   autofill::PopupBaseView)
 
  public:
+  enum class PopupViewId {
+    kNone = 0,
+    kConfirmButton,
+    kCancelButton,
+  };
+
   PasswordCrossDomainConfirmationPopupViewViews(
       base::WeakPtr<PasswordCrossDomainConfirmationPopupControllerInterface>
           controller,
@@ -50,6 +61,10 @@
   }
 
  private:
+  void OnConfirm(const ui::Event& event);
+
+  base::OnceClosure confirmation_callback_;
+  views::InputEventActivationProtector input_protector_;
   base::WeakPtrFactory<PasswordCrossDomainConfirmationPopupViewViews>
       weak_factory_{this};
 };
diff --git a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc
index 87b7bb0..bde00eb 100644
--- a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc
+++ b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc
@@ -16,6 +16,9 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/l10n/l10n_util.h"
+#include "ui/events/event.h"
+#include "ui/views/controls/button/md_text_button.h"
+#include "ui/views/test/button_test_api.h"
 #include "ui/views/widget/widget.h"
 #include "url/gurl.h"
 
@@ -69,6 +72,13 @@
   PasswordCrossDomainConfirmationPopupViewBrowsertest() = default;
   ~PasswordCrossDomainConfirmationPopupViewBrowsertest() override = default;
 
+ public:
+  bool confirm_called() const { return confirm_called_; }
+  void OnConfirm() { confirm_called_ = true; }
+
+ protected:
+  bool confirm_called_ = false;
+
   void SetUpOnMainThread() override {
     PopupPixelTest::SetUpOnMainThread();
 
@@ -98,7 +108,11 @@
         views::Widget::GetWidgetForNativeWindow(
             browser()->window()->GetNativeWindow()),
         /*domain=*/GURL("https://a.com"),
-        /*password_hostname=*/u"b.com", base::DoNothing(), base::DoNothing());
+        /*password_hostname=*/u"b.com",
+        base::BindOnce(
+            &PasswordCrossDomainConfirmationPopupViewBrowsertest::OnConfirm,
+            base::Unretained(this)),
+        base::DoNothing());
   }
 };
 
@@ -107,6 +121,44 @@
   ShowAndVerifyUi();
 }
 
+IN_PROC_BROWSER_TEST_P(PasswordCrossDomainConfirmationPopupViewBrowsertest,
+                       InitialFocusOnCancelButton) {
+  ShowUi("InitialFocusOnCancelButton");
+
+  views::MdTextButton* cancel_button =
+      views::AsViewClass<views::MdTextButton>(view()->GetViewByID(
+          static_cast<int>(PasswordCrossDomainConfirmationPopupViewViews::
+                               PopupViewId::kCancelButton)));
+  ASSERT_THAT(cancel_button, testing::NotNull());
+
+  views::View* initially_focused_view = view()->GetInitiallyFocusedView();
+  ASSERT_THAT(initially_focused_view, testing::NotNull());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc
index 87b7bb0..bde00eb 100644
--- a/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc
+++ b/chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views_browsertest.cc
@@ -16,6 +16,9 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/l10n/l10n_util.h"
+#include "ui/events/event.h"
+#include "ui/views/controls/button/md_text_button.h"
+#include "ui/views/test/button_test_api.h"
 #include "ui/views/widget/widget.h"
 #include "url/gurl.h"
 
@@ -69,6 +72,13 @@
   PasswordCrossDomainConfirmationPopupViewBrowsertest() = default;
   ~PasswordCrossDomainConfirmationPopupViewBrowsertest() override = default;
 
+ public:
+  bool confirm_called() const { return confirm_called_; }
+  void OnConfirm() { confirm_called_ = true; }
+
+ protected:
+  bool confirm_called_ = false;
+
   void SetUpOnMainThread() override {
     PopupPixelTest::SetUpOnMainThread();
 
@@ -98,7 +108,11 @@
         views::Widget::GetWidgetForNativeWindow(
             browser()->window()->GetNativeWindow()),
         /*domain=*/GURL("https://a.com"),
-        /*password_hostname=*/u"b.com", base::DoNothing(), base::DoNothing());
+        /*password_hostname=*/u"b.com",
+        base::BindOnce(
+            &PasswordCrossDomainConfirmationPopupViewBrowsertest::OnConfirm,
+            base::Unretained(this)),
+        base::DoNothing());
   }
 };
 
@@ -107,6 +121,44 @@
   ShowAndVerifyUi();
 }
 
+IN_PROC_BROWSER_TEST_P(PasswordCrossDomainConfirmationPopupViewBrowsertest,
+                       InitialFocusOnCancelButton) {
+  ShowUi("InitialFocusOnCancelButton");
+
+  views::MdTextButton* cancel_button =
+      views::AsViewClass<views::MdTextButton>(view()->GetViewByID(
+          static_cast<int>(PasswordCrossDomainConfirmationPopupViewViews::
+                               PopupViewId::kCancelButton)));
+  ASSERT_THAT(cancel_button, testing::NotNull());
+
+  views::View* initially_focused_view = view()->GetInitiallyFocusedView();
+  ASSERT_THAT(initially_focused_view, testing::NotNull());
+  EXPECT_EQ(initially_focused_view, cancel_button);
+}
+
+IN_PROC_BROWSER_TEST_P(PasswordCrossDomainConfirmationPopupViewBrowsertest,
+                       InputEventActivationProtectorWorks) {
+  ShowUi("InputProtectorWorks");
+
+  views::MdTextButton* confirm_button =
+      views::AsViewClass<views::MdTextButton>(view()->GetViewByID(
+          static_cast<int>(PasswordCrossDomainConfirmationPopupViewViews::
+                               PopupViewId::kConfirmButton)));
+  ASSERT_THAT(confirm_button, testing::NotNull());
+
+  ui::MouseEvent immediate_click(ui::EventType::kMousePressed, gfx::Point(),
+                                 gfx::Point(), base::TimeTicks::Now(),
+                                 ui::EF_LEFT_MOUSE_BUTTON, 0);
+  views::test::ButtonTestApi(confirm_button).NotifyClick(immediate_click);
+  EXPECT_FALSE(confirm_called());
+
+  ui::MouseEvent delayed_click(
+      ui::EventType::kMousePressed, gfx::Point(), gfx::Point(),
+      base::TimeTicks::Now() + base::Seconds(2), ui::EF_LEFT_MOUSE_BUTTON, 0);
+  views::test::ButtonTestApi(confirm_button).NotifyClick(delayed_click);
+  EXPECT_TRUE(confirm_called());
+}
+
 INSTANTIATE_TEST_SUITE_P(
     All,
     PasswordCrossDomainConfirmationPopupViewBrowsertest,
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential credential disclosure via keyjacking in cross-domain password confirmation popup

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 without the Chrome Security team.

Overview: The cross-domain password filling confirmation popup lacks an InputEventActivationProtector and auto-focuses the ‘Confirm’ button upon creation. This allows an attacker to bypass the security warning and steal cross-origin credentials by social-engineering a user into rapidly pressing or holding the ‘Enter’ key during manual fallback autofill.

Affected files:

  • chrome/browser/ui/views/passwords/password_cross_domain_confirmation_popup_view_views.cc
  • chrome/browser/ui/passwords/password_cross_domain_confirmation_popup_controller_impl.cc

Estimated timestamp from git blame: 2025-01-08

Description

The PasswordCrossDomainConfirmationPopupViewViews is a security-sensitive UI component designed to require explicit user consent before a saved credential from one origin (e.g., victim.example) is filled into another origin (e.g., attacker.example). However, this popup is potentially vulnerable to ‘keyjacking’ because it lacks standard dialog interaction protections.

The popup view is constructed as an activatable widget (views::Widget::InitParams::Activatable::kYes) and immediately sets the default focus to the prominent ‘Confirm’ button (SetInitiallyFocusedView(confirmation_button)). Crucially, because the view inherits from autofill::PopupBaseView (a views::WidgetDelegateView) and not views::DialogDelegateView, it does not benefit from the built-in InputEventActivationProtector typically provided by DialogClientView.

Exploitation Mechanism

When a user selects a cross-domain password suggestion (either from the manual-fallback picker or a grouped-affiliation suggestion) using the keyboard (e.g., pressing ‘Enter’), the autofill system synchronously processes the request. It detects the cross-domain nature and immediately creates and shows the confirmation popup.

Because the new popup is activatable, it steals native OS focus synchronously before the original autofill popup has fully closed. If an attacker can social-engineer the user into double-tapping or holding the ‘Enter’ key, the subsequent rapid or repeated key events are routed directly to the newly focused ‘Confirm’ button.

In the Views framework, ButtonController::OnKeyPressed does not inherently block event.is_repeat() events or enforce a debounce. On Windows, Linux, and ChromeOS, PlatformStyle::kReturnClicksFocusedControl is true, meaning the ‘Enter’ key triggers the button on press, not release. (On macOS, a similar vulnerability exists using the ‘Space’ key). Consequently, the auto-repeating or rapid second key press immediately executes the ‘Confirm’ callback without any delay, bypassing the security warning entirely.

Impact

A malicious website can potentially obtain a user’s saved credentials for another origin without requiring a renderer compromise. This relies on social engineering the user into specific keyboard interactions (e.g., “Hold Enter to verify”) while selecting a credential.

Suggested Reproduction Steps

Note: These are potential steps based on code analysis; automated tooling has not yet executed a live proof-of-concept.

  1. Ensure biometric authentication before filling is disabled (the default setting on desktop).
  2. Save a password for victim.example.
  3. Navigate to a malicious page attacker.example containing a login form.
  4. The page instructs the user: “Right-click the box, select your account, and hold Enter to verify.”
  5. The user triggers manual-fallback autofill, selects the victim.example entry, and holds the ‘Enter’ key.
  6. The first key press accepts the suggestion. The cross-domain popup appears and steals focus.
  7. The subsequent repeating key events immediately trigger the focused ‘Confirm’ button.
  8. The credential is filled into the attacker’s page and extracted via JavaScript.

Suggested Fix

  1. Implement Input Protection: The view should utilize an InputEventActivationProtector to ignore input events for a short period (e.g., 500ms) after the popup becomes visible. This is the standard defense against clickjacking and keyjacking in security dialogs. This could be achieved by refactoring the popup to use DialogDelegateView or by manually instantiating and checking an InputEventActivationProtector in the view’s event handlers.
  2. Adjust Default Focus: Consider altering SetInitiallyFocusedView so that the ‘Confirm’ button is not focused by default. Focusing the ‘Cancel’ button or nothing at all would prevent accidental confirmation via rapid ‘Enter’ presses.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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