CVE-2026-79011
Overview
Background
- `BluetoothDevicePairConfirmView`
- the tab-modal Views dialog (
chrome/browser/ui/views/bluetooth_device_pair_confirm_view.cc) that asks the user to confirm pairing with a Bluetooth device, optionally showing a passkey. Its result goes back viaBluetoothDelegate::PairPromptResult. - `InputEventActivationProtector`
- Views machinery that ignores dialog-button activations arriving within a short window (500 ms per the commit) after the dialog is shown, so a click or keystroke aimed at the page cannot land on a newly appeared security prompt.
- `ShouldAllowKeyEventsDuringInputProtection()`
- a
DialogDelegatevirtual that decides whether key events are exempt from that protection window. If it returns true, keyboard activation is accepted at once. - Keyjacking
- a UI-redress technique where a page gets the user typing or holding a key (e.g. Enter or Space) and then makes a permission or confirmation dialog appear, so the keystroke already in progress accepts it.
Root Cause Analysis
BluetoothDevicePairConfirmView did not override ShouldAllowKeyEventsDuringInputProtection(), so it inherited the DialogDelegate behaviour that exempts keyboard events from the InputEventActivationProtector safety window. Mouse activations right after the dialog appeared were filtered, but an Enter keypress delivered at once to the default OK button was accepted and ran OnDialogAccepted(), which reports PairPromptStatus::kSuccess. That broke the rule that a security-sensitive confirmation needs deliberate user input made after the user could see the dialog.
The fix adds an override that returns false, so key events are also subject to the 500 ms window. The regression test checks this: an Enter keypress timestamped “now” leaves the dialog open with no result, and one 600 ms later accepts it.
false from ShouldAllowKeyEventsDuringInputProtection() closes that gap.Attack Path
- Lure the user into typing A malicious page asks the user to press or hold Enter or Space, for example through a fake game or form.
- Trigger a pairing prompt
The page uses Web Bluetooth so that Chrome shows
BluetoothDevicePairConfirmViewfor a device that needs pairing. This code is only built whenPAIR_BLUETOOTH_ON_DEMAND()is set (a platform-specific build). It most likely also needs a prior user gesture and device selection in the Bluetooth chooser, plus a pairable device in range (inferred). - Keystroke lands on the dialog
Before the fix, the Enter key event that arrived as the dialog appeared activated the OK button, because key events were exempt from the
InputEventActivationProtectorwindow. - Pairing is confirmed without informed consent
OnDialogAccepted()returnskSuccessto theBluetoothDelegatecallback and pairing goes ahead. What a site can then do with a paired device is beyond what the patch shows (inferred).
Impact Assessment
(inferred).Files Changed
chrome/browser/ui/views/bluetooth_device_pair_confirm_view.ccchrome/browser/ui/views/bluetooth_device_pair_confirm_view.hchrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc
Audit Directions
- Other security dialogs inheriting key-event exemptionList every
DialogDelegate/BubbleDialogDelegatesubclass that guards a permission, device, download or credential decision. Confirm it either overridesShouldAllowKeyEventsDuringInputProtection()tofalseor has equivalent protection. - Default-button dialogs raised by web contentPrompts that web content can open on demand and whose default button means “allow” are the main keyjacking targets. Check whether the default button should be “cancel”, or whether a protection window applies.
- Input-protection edge casesCheck that
InputEventActivationProtectortiming restarts when a dialog is re-shown, moved, or made visible again, so a page cannot pre-arm the window by opening the dialog while it is hidden.
Patch
From f132e7c0896d4353e2c2c1adcb019e6eb43a92c1 Mon Sep 17 00:00:00 2001 From: Alvin Ji <[email protected]> Date: Mon, 20 Jul 2026 13:58:33 -0700 Subject: [PATCH] bluetooth: Protect BluetoothDevicePairConfirmView against keyjacking Overrides ShouldAllowKeyEventsDuringInputProtection to false in BluetoothDevicePairConfirmView. This enforces the InputEventActivationProtector 500ms safety window for all initial keyboard events to prevent accidental or social-engineered instant dialog acceptance. BUG=517736936 Change-Id: I65184125da21a835c664c0f7a437a630887dadb6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8108664 Reviewed-by: Darryl James <[email protected]> Commit-Queue: Alvin Ji <[email protected]> Cr-Commit-Position: refs/heads/main@{#1664924} --- diff --git a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.cc b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.cc index 2c7a88f..71e7831d 100644 --- a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.cc +++ b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.cc @@ -161,6 +161,11 @@ IDS_BLUETOOTH_DEVICE_PAIR_CONFIRM_TITLE); } +bool BluetoothDevicePairConfirmView::ShouldAllowKeyEventsDuringInputProtection() + const { + return false; +} + void BluetoothDevicePairConfirmView::OnDialogAccepted() { BluetoothDelegate::PairPromptResult prompt_result; prompt_result.result_code = BluetoothDelegate::PairPromptStatus::kSuccess; diff --git a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.h b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.h index 1e1fff2..990a57c6 100644 --- a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.h +++ b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view.h @@ -39,6 +39,9 @@ // WidgetDelegate: std::u16string GetWindowTitle() const override; + // DialogDelegate: + bool ShouldAllowKeyEventsDuringInputProtection() const override; + private: // Runs the |close_callback_| with the PairPromptResult if the dialog is // accepted. diff --git a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc index cfd4141..68b7a2c3 100644 --- a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc +++ b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc @@ -2,17 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "chrome/browser/ui/views/bluetooth_device_pair_confirm_view.h" + #include <string> #include "base/functional/callback_helpers.h" -#include "base/strings/utf_string_conversions.h" +#include "base/test/test_future.h" #include "chrome/browser/ui/bluetooth/bluetooth_dialogs.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/test/test_browser_dialog.h" +#include "components/constrained_window/constrained_window_views.h" #include "content/public/browser/bluetooth_delegate.h" #include "content/public/browser/web_contents.h" #include "content/public/test/browser_test.h" +#include "ui/events/base_event_utils.h" +#include "ui/views/test/button_test_api.h" #if PAIR_BLUETOOTH_ON_DEMAND() @@ -51,6 +56,53 @@ ShowAndVerifyUi(); } +IN_PROC_BROWSER_TEST_P(BluetoothDevicePairConfirmViewBrowserTest, + KeyjackingProtectionSafetyWindow) { + base::test::TestFuture<content::BluetoothDelegate::PairPromptResult> future; + + auto passkey = + DisplayPasskey() ? std::optional<std::u16string>(kPasskey) : std::nullopt; + + // 1. Instantiate the view and show it using the constrained_window API + // directly. ShowWebModalDialogViews returns the Widget* pointer directly! + auto* view = new BluetoothDevicePairConfirmView( + kDeviceIdentifier, passkey, + future + .GetCallback<const content::BluetoothDelegate::PairPromptResult&>()); + views::Widget* dialog_widget = constrained_window::ShowWebModalDialogViews( + view, browser()->tab_strip_model()->GetActiveWebContents()); + ASSERT_NE(dialog_widget, nullptr); + + views::MdTextButton* ok_button = view->GetOkButton(); + ASSERT_NE(ok_button, nullptr); + + // 2. PRESS ENTER IMMEDIATELY (within the 500ms safety window) + ui::KeyEvent press_enter_soon(ui::EventType::kKeyPressed, ui::VKEY_RETURN, + ui::EF_NONE, ui::EventTimeForNow()); + views::test::ButtonTestApi(ok_button).NotifyClick(press_enter_soon); + + // VERIFY: The dialog should NOT have closed (input was ignored) + EXPECT_FALSE(dialog_widget->IsClosed()); + EXPECT_FALSE(future.IsReady()); + + // 3. PRESS ENTER AFTER the safety window (Offset by 600ms) + ui::KeyEvent press_enter_later( + ui::EventType::kKeyPressed, ui::VKEY_RETURN, ui::EF_NONE, + ui::EventTimeForNow() + base::Milliseconds(600)); + views::test::ButtonTestApi(ok_button).NotifyClick(press_enter_later); + + // VERIFY: The dialog SHOULD be closed now + EXPECT_TRUE(dialog_widget->IsClosed()); + ASSERT_TRUE(future.IsReady()); + EXPECT_EQ(future.Get().result_code, + content::BluetoothDelegate::PairPromptStatus::kSuccess); + + // If the test fails and the dialog is somehow not closed, clean it up + if (!dialog_widget->IsClosed()) { + dialog_widget->CloseNow(); + } +} + INSTANTIATE_TEST_SUITE_P(All, BluetoothDevicePairConfirmViewBrowserTest, testing::Bool());
Regression Test / PoC
diff --git a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc
index cfd4141..68b7a2c3 100644
--- a/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc
+++ b/chrome/browser/ui/views/bluetooth_device_pair_confirm_view_browsertest.cc
@@ -2,17 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "chrome/browser/ui/views/bluetooth_device_pair_confirm_view.h"
+
#include <string>
#include "base/functional/callback_helpers.h"
-#include "base/strings/utf_string_conversions.h"
+#include "base/test/test_future.h"
#include "chrome/browser/ui/bluetooth/bluetooth_dialogs.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/test/test_browser_dialog.h"
+#include "components/constrained_window/constrained_window_views.h"
#include "content/public/browser/bluetooth_delegate.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
+#include "ui/events/base_event_utils.h"
+#include "ui/views/test/button_test_api.h"
#if PAIR_BLUETOOTH_ON_DEMAND()
@@ -51,6 +56,53 @@
ShowAndVerifyUi();
}
+IN_PROC_BROWSER_TEST_P(BluetoothDevicePairConfirmViewBrowserTest,
+ KeyjackingProtectionSafetyWindow) {
+ base::test::TestFuture<content::BluetoothDelegate::PairPromptResult> future;
+
+ auto passkey =
+ DisplayPasskey() ? std::optional<std::u16string>(kPasskey) : std::nullopt;
+
+ // 1. Instantiate the view and show it using the constrained_window API
+ // directly. ShowWebModalDialogViews returns the Widget* pointer directly!
+ auto* view = new BluetoothDevicePairConfirmView(
+ kDeviceIdentifier, passkey,
+ future
+ .GetCallback<const content::BluetoothDelegate::PairPromptResult&>());
+ views::Widget* dialog_widget = constrained_window::ShowWebModalDialogViews(
+ view, browser()->tab_strip_model()->GetActiveWebContents());
+ ASSERT_NE(dialog_widget, nullptr);
+
+ views::MdTextButton* ok_button = view->GetOkButton();
+ ASSERT_NE(ok_button, nullptr);
+
+ // 2. PRESS ENTER IMMEDIATELY (within the 500ms safety window)
+ ui::KeyEvent press_enter_soon(ui::EventType::kKeyPressed, ui::VKEY_RETURN,
+ ui::EF_NONE, ui::EventTimeForNow());
+ views::test::ButtonTestApi(ok_button).NotifyClick(press_enter_soon);
+
+ // VERIFY: The dialog should NOT have closed (input was ignored)
+ EXPECT_FALSE(dialog_widget->IsClosed());
+ EXPECT_FALSE(future.IsReady());
+
+ // 3. PRESS ENTER AFTER the safety window (Offset by 600ms)
+ ui::KeyEvent press_enter_later(
+ ui::EventType::kKeyPressed, ui::VKEY_RETURN, ui::EF_NONE,
+ ui::EventTimeForNow() + base::Milliseconds(600));
+ views::test::ButtonTestApi(ok_button).NotifyClick(press_enter_later);
+
+ // VERIFY: The dialog SHOULD be closed now
+ EXPECT_TRUE(dialog_widget->IsClosed());
+ ASSERT_TRUE(future.IsReady());
+ EXPECT_EQ(future.Get().result_code,
+ content::BluetoothDelegate::PairPromptStatus::kSuccess);
+
+ // If the test fails and the dialog is somehow not closed, clean it up
+ if (!dialog_widget->IsClosed()) {
+ dialog_widget->CloseNow();
+ }
+}
+
INSTANTIATE_TEST_SUITE_P(All,
BluetoothDevicePairConfirmViewBrowserTest,
testing::Bool());
Original Bug Report
Potential Keyjacking of Web Bluetooth pair confirmation dialog via default OK button focus
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: The BluetoothDevicePairConfirmView dialog defaults to focusing the OK button and permits key events during the input protection period. An attacker-controlled page, cooperating with a malicious Bluetooth peripheral, can potentially social-engineer keypresses to instantly accept the pairing prompt. This allows a remote Bluetooth device to establish an authenticated bond without the user comparing or verifying the passkey.
Affected files:
chrome/browser/ui/views/bluetooth_device_pair_confirm_view.ccchrome/browser/ui/views/bluetooth_device_pair_confirm_view.h
Estimated timestamp from git blame: 2022-06-08
Summary and Root Cause Analysis
There is a potential UI/input-protection bypass (keyjacking) vulnerability in BluetoothDevicePairConfirmView (chrome/browser/ui/views/bluetooth_device_pair_confirm_view.cc). When the dialog is shown to confirm secure Bluetooth pairing, it defaults to focusing the “OK” button and permits key events during the input protection period. An attacker could leverage this to instantly accept the pairing prompt via social-engineered keystrokes (such as Space or Enter), bypassing the Secure Simple Pairing (SSP) Numeric Comparison verification.
BluetoothDevicePairConfirmView inherits from views::DialogDelegateView but does not override ShouldAllowKeyEventsDuringInputProtection(). Because of this, the default behaviors from DialogDelegate apply:
GetInitiallyFocusedView()defaults to returning the “OK” button.ShouldAllowKeyEventsDuringInputProtection()returnstrue:bool DialogDelegate::ShouldAllowKeyEventsDuringInputProtection() const { return true; }
When the dialog is shown tab-modally using constrained_window::ShowWebModalDialogViews, the tab-modal manager (NativeWebContentsModalDialogManagerViews::Focus) immediately requests keyboard focus on the “OK” button.
If a key event (such as VKEY_RETURN or VKEY_SPACE) is dispatched while the button is focused, Button::NotifyClick triggers the click callback. During verification of input protection in DialogClientView::ButtonPressed, the input protector checks:
if (!delegate ||
input_protector_->IsPossiblyUnintendedInteraction(
event, /*allow_key_events=*/delegate
->ShouldAllowKeyEventsDuringInputProtection())) {
return;
}
Because allow_key_events is true, IsPossiblyUnintendedInteraction in ui/views/input_event_activation_protector.cc permits the key event to bypass the safety time-gate check (which is designed to prevent click/tap interactions for the first 500 ms):
if (event.IsKeyEvent() && event.AsKeyEvent()->is_repeat()) {
return true;
}
if (!event.IsMouseEvent() && !event.IsTouchEvent() && !event.IsGestureEvent()) {
if (allow_key_events || !event.IsKeyEvent()) {
return false; // Non-repeat key event allowed immediately, bypassing the 500 ms check
}
}
Consequently, the first non-repeat Enter or Space press arriving after the dialog is shown executes AcceptDialog(), triggering BluetoothDevicePairConfirmView::OnDialogAccepted() which confirms the pairing.
Potential Attack Scenario
Note: These are potential steps that an attacker might follow. Our tooling agent does not currently have the ability to execute code or verify the exploit end-to-end on running systems.
- The victim grants Web Bluetooth chooser access to an attacker-controlled BLE peripheral (e.g., via
navigator.bluetooth.requestDevice()). - The attacker’s page social-engineers the user into typing or repeatedly pressing
EnterorSpace(such as in an embedded mini-game). - The page initiates a read operation on an authenticated/encryption-required characteristic.
- The BLE peripheral rejects the read with
Insufficient Authentication. On platforms utilizing Web Bluetooth on-demand pairing (Windows, Linux, and ChromeOS), this triggersOnCharacteristicReadValue->PairForCharacteristicReadValue->PromptForBluetoothPairing. - The pairing dialog (
BluetoothDevicePairConfirmView) appears, instantly stealing focus to the “OK” button. - A pending or newly-pressed non-repeat Enter or Space key event executes
AcceptDialog()without requiring the user to read the 6-digit passkey or confirm the pairing manually. - Once paired, the malicious BLE peripheral can present itself to the operating system as a trusted Bluetooth HID keyboard, injecting keystrokes to compromise the host OS outside of the browser sandbox.
Suggested Fix
To remediate this, BluetoothDevicePairConfirmView should override ShouldAllowKeyEventsDuringInputProtection() to return false (similar to ExternalProtocolDialog or ExtensionInstallDialogView):
bool BluetoothDevicePairConfirmView::ShouldAllowKeyEventsDuringInputProtection() const {
return false;
}
This ensures that the 500 ms InputEventActivationProtector time-gate is enforced for all keyboard events, preventing accidental activation or automated keyjacking.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.