CVE-2026-14144
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc |
modified | |
TEST_Fui/views/bubble/bubble_dialog_model_host_unittest.cc |
modified |
Files Changed
chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.ccui/base/models/dialog_model.hui/views/bubble/bubble_dialog_model_host.ccui/views/bubble/bubble_dialog_model_host.hui/views/bubble/bubble_dialog_model_host_unittest.cc
Patch
From e64e0f3802d5c7166a374f70ed21adae5f9d33df Mon Sep 17 00:00:00 2001 From: Mohamed Amir Yosef <[email protected]> Date: Tue, 19 May 2026 12:40:46 -0700 Subject: [PATCH] [Views] Allow dialog models to protect key events from keyjacking Currently, key events bypass the dialog input event activation protector (which imposes a 500ms safety delay to prevent accidental confirmation). This bypass poses a keyjacking security risk for highly sensitive safety interstitial dialogs, such as the Digital Identity safety interstitial on desktop, where a malicious page could trick a user into accidentally approving the dialog via rapid keystrokes. This CL adds SetShouldAllowKeyEventsDuringInputProtection(bool) to the ui::DialogModel::Builder. BubbleDialogModelHost overrides ShouldAllowKeyEventsDuringInputProtection() to honor this configuration, and the Digital Identity safety interstitial is updated to enable it. Fixed: b:514079793 Test: views_unittests --gtest_filter=BubbleDialogModelHostTest.ShouldAllowKeyEventsDuringInputProtection Change-Id: I4e8ca5ab99cbee4a24f6fa5e17750dd5ad05ddca Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852411 Reviewed-by: Dana Fried <[email protected]> Auto-Submit: Mohamed Amir Yosef <[email protected]> Reviewed-by: Thomas Lukaszewicz <[email protected]> Commit-Queue: Mohamed Amir Yosef <[email protected]> Cr-Commit-Position: refs/heads/main@{#1633062} --- diff --git a/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc b/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc index c851f517..05719fe 100644 --- a/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc +++ b/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc @@ -159,7 +159,8 @@ .SetTitle(l10n_util::GetStringUTF16( IDS_WEB_DIGITAL_CREDENTIALS_INTERSTITIAL_DIALOG_TITLE)) .AddParagraph(ui::DialogModelLabel(body_text)) - .SetInitiallyFocusedField(kContinueButtonId); + .SetInitiallyFocusedField(kContinueButtonId) + .SetEnableInputProtection(true); if (was_request_aborted) { dialog_model_builder.AddParagraph( diff --git a/ui/base/models/dialog_model.h b/ui/base/models/dialog_model.h index 325e7eb0..705a7d6 100644 --- a/ui/base/models/dialog_model.h +++ b/ui/base/models/dialog_model.h @@ -262,6 +262,11 @@ return *this; } + Builder& SetEnableInputProtection(bool enable) { + model_->enable_input_protection_ = enable; + return *this; + } + // Disables the default behavior that the dialog closes when Escape is // pressed. // Only certain dialogs are allowed to change this properly, as it has @@ -652,6 +657,10 @@ return close_on_escape_; } + bool enable_input_protection(base::PassKey<DialogModelHost>) const { + return enable_input_protection_; + } + DialogModelSection* contents() { return &contents_; } DialogModelDelegate* delegate() { return delegate_.get(); } @@ -675,6 +684,7 @@ std::optional<bool> override_show_close_button_; bool close_on_deactivate_ = true; bool close_on_escape_ = true; + bool enable_input_protection_ = false; std::string internal_name_; std::u16string title_; std::u16string accessible_title_; diff --git a/ui/views/bubble/bubble_dialog_model_host.cc b/ui/views/bubble/bubble_dialog_model_host.cc index 7e6417b..1db7d43 100644 --- a/ui/views/bubble/bubble_dialog_model_host.cc +++ b/ui/views/bubble/bubble_dialog_model_host.cc @@ -1016,6 +1016,12 @@ return BubbleDialogDelegate::OnCloseRequested(close_reason); } +bool BubbleDialogModelHost::ShouldAllowKeyEventsDuringInputProtection() const { + return model_ + ? !model_->enable_input_protection(DialogModelHost::GetPassKey()) + : true; +} + BubbleDialogModelHost::~BubbleDialogModelHost() { // Detach ContentsView as it's referring to state that's about to be // destroyed. diff --git a/ui/views/bubble/bubble_dialog_model_host.h b/ui/views/bubble/bubble_dialog_model_host.h index ca30279..4d234002 100644 --- a/ui/views/bubble/bubble_dialog_model_host.h +++ b/ui/views/bubble/bubble_dialog_model_host.h @@ -117,6 +117,7 @@ View* GetInitiallyFocusedView() override; void OnWidgetInitialized() override; bool OnCloseRequested(views::Widget::ClosedReason close_reason) override; + bool ShouldAllowKeyEventsDuringInputProtection() const override; // ui::DialogModelHost: void Close() override; diff --git a/ui/views/bubble/bubble_dialog_model_host_unittest.cc b/ui/views/bubble/bubble_dialog_model_host_unittest.cc index 21c3c4d..aad0f37 100644 --- a/ui/views/bubble/bubble_dialog_model_host_unittest.cc +++ b/ui/views/bubble/bubble_dialog_model_host_unittest.cc @@ -480,4 +480,27 @@ EXPECT_TRUE(bubble_widget->IsClosed()); } +TEST_F(BubbleDialogModelHostTest, ShouldAllowKeyEventsDuringInputProtection) { + std::unique_ptr<Widget> anchor_widget = CreateTestWidget( + Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); + anchor_widget->Show(); + + std::unique_ptr<ui::DialogModel> dialog_model_default = + ui::DialogModel::Builder().AddOkButton(base::DoNothing()).Build(); + auto host_default = std::make_unique<BubbleDialogModelHost>( + std::move(dialog_model_default), anchor_widget->GetContentsView(), + BubbleBorder::Arrow::TOP_RIGHT); + EXPECT_TRUE(host_default->ShouldAllowKeyEventsDuringInputProtection()); + + std::unique_ptr<ui::DialogModel> dialog_model_false = + ui::DialogModel::Builder() + .AddOkButton(base::DoNothing()) + .SetEnableInputProtection(true) + .Build(); + auto host_false = std::make_unique<BubbleDialogModelHost>( + std::move(dialog_model_false), anchor_widget->GetContentsView(), + BubbleBorder::Arrow::TOP_RIGHT); + EXPECT_FALSE(host_false->ShouldAllowKeyEventsDuringInputProtection()); +} + } // namespace views
Regression Test / PoC
diff --git a/ui/views/bubble/bubble_dialog_model_host_unittest.cc b/ui/views/bubble/bubble_dialog_model_host_unittest.cc
index 21c3c4d..aad0f37 100644
--- a/ui/views/bubble/bubble_dialog_model_host_unittest.cc
+++ b/ui/views/bubble/bubble_dialog_model_host_unittest.cc
@@ -480,4 +480,27 @@
EXPECT_TRUE(bubble_widget->IsClosed());
}
+TEST_F(BubbleDialogModelHostTest, ShouldAllowKeyEventsDuringInputProtection) {
+ std::unique_ptr<Widget> anchor_widget = CreateTestWidget(
+ Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
+ anchor_widget->Show();
+
+ std::unique_ptr<ui::DialogModel> dialog_model_default =
+ ui::DialogModel::Builder().AddOkButton(base::DoNothing()).Build();
+ auto host_default = std::make_unique<BubbleDialogModelHost>(
+ std::move(dialog_model_default), anchor_widget->GetContentsView(),
+ BubbleBorder::Arrow::TOP_RIGHT);
+ EXPECT_TRUE(host_default->ShouldAllowKeyEventsDuringInputProtection());
+
+ std::unique_ptr<ui::DialogModel> dialog_model_false =
+ ui::DialogModel::Builder()
+ .AddOkButton(base::DoNothing())
+ .SetEnableInputProtection(true)
+ .Build();
+ auto host_false = std::make_unique<BubbleDialogModelHost>(
+ std::move(dialog_model_false), anchor_widget->GetContentsView(),
+ BubbleBorder::Arrow::TOP_RIGHT);
+ EXPECT_FALSE(host_false->ShouldAllowKeyEventsDuringInputProtection());
+}
+
} // namespace views
Original Bug Report
Keyjacking bypass of Digital Identity safety interstitial on Desktop
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A malicious website can potentially bypass the Digital Identity safety interstitial on desktop by tricking a user into accidentally accepting the dialog via rapid keystrokes. This is due to the ‘Continue’ button being focused by default and a lack of key-event input protection in the dialog host.
Affected files:
chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.ccui/views/bubble/bubble_dialog_model_host.ccui/views/input_event_activation_protector.cc
Estimated timestamp from git blame: 2025-07-07
Background
The Digital Identity API (navigator.credentials.get({digital: ...})) is used by websites to request sensitive information from a user’s digital wallet. On desktop, Chrome displays a safety interstitial (a modal security warning) to ensure the user is aware of the risks before proceeding with a cross-device request (e.g., scanning a QR code).
The Issue
A potential vulnerability in the desktop implementation of this interstitial allows a malicious site to bypass the warning through a ‘keyjacking’ attack.
-
Initially Focused Button: In
DigitalIdentitySafetyInterstitialControllerDesktop::ShowInterstitialImpl, the dialog is constructed usingui::DialogModeland explicitly sets the initial focus to the ‘Continue’ button:.SetInitiallyFocusedField(kContinueButtonId);(digital_identity_safety_interstitial_controller_desktop.cc:162). -
Input Protection Bypass: The dialog is hosted by
views::BubbleDialogModelHost, which inherits the default behavior ofviews::DialogDelegate::ShouldAllowKeyEventsDuringInputProtection(). This default implementation returnstrue(dialog_delegate.cc:249). -
Protector Logic: When a user interaction occurs,
views::InputEventActivationProtector::IsPossiblyUnintendedInteraction()is called to prevent accidental interactions immediately after a dialog appears (within a ~500ms window). However, the protector explicitly allows key events to bypass this time-gate if the delegate’sallow_key_eventsis set totrue:if (!event.IsMouseEvent() && !event.IsTouchEvent() && !event.IsGestureEvent()) { if (allow_key_events || !event.IsKeyEvent()) { return false; // Bypasses the 500ms safety interval } }
Consequently, any non-repeat key event (such as pressing Enter or Space) is processed immediately upon the dialog’s appearance, bypassing the guard designed to ensure informed user consent.
Potential Attack Scenario
- A malicious page induces the user to perform rapid keystrokes (e.g., via a fake ’type-to-win’ game or CAPTCHA requiring repeated
Enterpresses). - During this activity, the page calls
navigator.credentials.get({digital: ...}), consuming the transient user activation requirement. - The browser process displays the Digital Identity safety interstitial.
- The ‘Continue’ button receives keyboard focus the instant the dialog appears due to the
SetInitiallyFocusedFieldconfiguration. - The user’s next keystroke (intended for the attacker’s page) is instead delivered to the focused ‘Continue’ button.
- The
InputEventActivationProtectorallows the interaction immediately, bypassing the safety interval. - The dialog is dismissed, and the browser proceeds to the next step (e.g., displaying a QR code for a mobile wallet), having bypassed the safety warning without the user’s informed consent.
Note: These steps are suggested based on code analysis; our current environment does not support running live proof-of-concept code.
Impact
While this bypass allows skipping the browser’s safety warning, the overall impact is mitigated by the fact that the user must still complete the subsequent flow (e.g., scanning a QR code on a mobile device and approving the request in a wallet app). However, bypassing a primary security interstitial is considered a Medium (S2) severity issue in Chrome.
Suggested Fix
The host for the Digital Identity safety interstitial should ensure that ShouldAllowKeyEventsDuringInputProtection() returns false. This ensures that key events are subject to the same 500ms input protection interval as mouse events, preventing accidental dismissal.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc;l=162
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/input_event_activation_protector.cc;l=60
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/window/dialog_delegate.cc;l=249