CVE-2026-79011
Overview
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
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.