CVE-2026-106263
Overview
Files Changed
chrome/browser/ui/signin/signin_view_controller.ccchrome/browser/ui/signin/signin_view_controller_browsertest.cc
Patch
From 19f5d15d292cda31b378ebd1de073cc1ecb4f5de Mon Sep 17 00:00:00 2001
From: Ernest Nguyen Hung <ernn@google.com>
Date: Wed, 02 Sep 2026 01:51:03 -0700
Subject: [PATCH] [Signin] Update extension dialog to prevent from unintended key press
By default, `ChromeSigninChoiceForExtensionsPrompt` focuses on a confirm
button. This can lead to an unintended browser sign in.
Bug: 553149001
Change-Id: I0b372b20b5bf10c5b63ef115d368bd05cc60421a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8320447
Reviewed-by: David Roger <droger@chromium.org>
Commit-Queue: Ernest Nguyen Hung <ernn@google.com>
Cr-Commit-Position: refs/heads/main@{#1690535}
---
diff --git a/chrome/browser/ui/signin/signin_view_controller.cc b/chrome/browser/ui/signin/signin_view_controller.cc
index 80f50718..67ddae0 100644
--- a/chrome/browser/ui/signin/signin_view_controller.cc
+++ b/chrome/browser/ui/signin/signin_view_controller.cc
@@ -977,6 +977,7 @@
.AddCancelButton(base::DoNothing(),
ui::DialogModel::Button::Params().SetLabel(
l10n_util::GetStringUTF16(IDS_CANCEL)))
+ .SetEnableInputProtection(true)
.SetDialogDestroyingCallback(std::move(on_complete));
chrome::ShowTabModal(dialog_builder.Build(), contents);
diff --git a/chrome/browser/ui/signin/signin_view_controller_browsertest.cc b/chrome/browser/ui/signin/signin_view_controller_browsertest.cc
index ae43a0f..3a83952 100644
--- a/chrome/browser/ui/signin/signin_view_controller_browsertest.cc
+++ b/chrome/browser/ui/signin/signin_view_controller_browsertest.cc
@@ -6,6 +6,7 @@
#include <string_view>
+#include "base/functional/callback_helpers.h"
#include "base/scoped_observation.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
@@ -85,6 +86,9 @@
namespace {
+using ::testing::IsEmpty;
+using ::testing::Not;
+
constexpr char kTestEmail[] = "email@gmail.com";
constexpr signin_metrics::AccessPoint kTestAccessPoint =
signin_metrics::AccessPoint::kProfileMenuSignoutConfirmationPrompt;
@@ -774,6 +778,25 @@
IN_PROC_BROWSER_TEST_F(
SigninViewControllerBrowserTest,
+ ShowChromeSigninDialogForExtensionsPromptKeyjackingProtection) {
+ // Make sure a user is signed in to web (otherwise the dialog doesn't
+ // trigger).
+ identity_test_env()->MakeAccountAvailable(kTestEmail, {.set_cookie = true});
+ ASSERT_THAT(identity_manager()->GetAccountsWithRefreshTokens(),
+ Not(IsEmpty()));
+ ASSERT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+
+ views::DialogDelegate* dialog_delegate =
+ TriggerChromeSigninDialogForExtensionsPrompt(
+ /*on_complete=*/base::DoNothing());
+
+ ASSERT_NE(dialog_delegate, nullptr);
+ EXPECT_FALSE(dialog_delegate->ShouldAllowKeyEventsDuringInputProtection());
+}
+
+IN_PROC_BROWSER_TEST_F(
+ SigninViewControllerBrowserTest,
ShowChromeSigninDialogForExtensionsPromptNotShownPrimaryAccountSet) {
identity_test_env()->MakePrimaryAccountAvailable(
kTestEmail, signin::ConsentLevel::kSignin);
Regression Test / PoC
diff --git a/chrome/browser/ui/signin/signin_view_controller_browsertest.cc b/chrome/browser/ui/signin/signin_view_controller_browsertest.cc
index ae43a0f..3a83952 100644
--- a/chrome/browser/ui/signin/signin_view_controller_browsertest.cc
+++ b/chrome/browser/ui/signin/signin_view_controller_browsertest.cc
@@ -6,6 +6,7 @@
#include <string_view>
+#include "base/functional/callback_helpers.h"
#include "base/scoped_observation.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
@@ -85,6 +86,9 @@
namespace {
+using ::testing::IsEmpty;
+using ::testing::Not;
+
constexpr char kTestEmail[] = "email@gmail.com";
constexpr signin_metrics::AccessPoint kTestAccessPoint =
signin_metrics::AccessPoint::kProfileMenuSignoutConfirmationPrompt;
@@ -774,6 +778,25 @@
IN_PROC_BROWSER_TEST_F(
SigninViewControllerBrowserTest,
+ ShowChromeSigninDialogForExtensionsPromptKeyjackingProtection) {
+ // Make sure a user is signed in to web (otherwise the dialog doesn't
+ // trigger).
+ identity_test_env()->MakeAccountAvailable(kTestEmail, {.set_cookie = true});
+ ASSERT_THAT(identity_manager()->GetAccountsWithRefreshTokens(),
+ Not(IsEmpty()));
+ ASSERT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+
+ views::DialogDelegate* dialog_delegate =
+ TriggerChromeSigninDialogForExtensionsPrompt(
+ /*on_complete=*/base::DoNothing());
+
+ ASSERT_NE(dialog_delegate, nullptr);
+ EXPECT_FALSE(dialog_delegate->ShouldAllowKeyEventsDuringInputProtection());
+}
+
+IN_PROC_BROWSER_TEST_F(
+ SigninViewControllerBrowserTest,
ShowChromeSigninDialogForExtensionsPromptNotShownPrimaryAccountSet) {
identity_test_env()->MakePrimaryAccountAvailable(
kTestEmail, signin::ConsentLevel::kSignin);
Original Bug Report
Enter-keyjacking in ChromeSigninChoiceForExtensionsPrompt enables unauthorized sign-in
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 ChromeSigninChoiceForExtensionsPrompt dialog default-focuses the ‘Continue as <user>’ OK button and lacks keyboard input protection. An installed extension can trigger this consent prompt while a user is typing or holding the Enter key, causing an unintended confirmation on key press. This potentially allows an extension to elevate a web-only account to the browser’s primary account and obtain the user’s identity information without explicit consent.
Affected files:
chrome/browser/ui/signin/signin_view_controller.cc
Estimated timestamp from git blame: 2024-06-17
Potential enter-keyjacking in ChromeSigninChoiceForExtensionsPrompt
Location
chrome/browser/ui/signin/signin_view_controller.cc:916-974
Summary
SigninViewController::ShowChromeSigninDialogForExtensions constructs the ChromeSigninChoiceForExtensionsPrompt dialog using ui::DialogModel::Builder. This dialog gates whether an installed extension can elevate a web-only signed-in Google account into Chrome’s primary browser account. However, the builder omits OverrideDefaultButton(), SetInitiallyFocusedField(), and SetEnableInputProtection(true).
Due to Chromium framework defaults:
- OK is the default button & initially focused view:
DialogDelegate::GetDefaultDialogButton()defaults to OK (ui/views/window/dialog_delegate.cc:191-202), andBubbleDialogModelHost::GetInitiallyFocusedView()falls through toDialogDelegate::GetInitiallyFocusedView()(ui/views/window/dialog_delegate.cc:292-317), focusing the OK button upon display (components/constrained_window/native_web_contents_modal_dialog_manager_views.cc:140-150). - Keyboard events bypass input protection:
BubbleDialogModelHost::ShouldAllowKeyEventsDuringInputProtection()returnstruebecauseenable_input_protection_is false (ui/views/bubble/bubble_dialog_model_host.cc:1044-1048). Consequently,DialogClientView::ButtonPressedpassesallow_key_events=truetoInputEventActivationProtector::IsPossiblyUnintendedInteraction(), which immediately returnsfalsefor key events (ui/views/input_event_activation_protector.cc:66-72), skipping the 500ms cooldown and repeat-filtering protection. - Key press executes click: On non-macOS platforms (
!BUILDFLAG(IS_MAC)),PlatformStyle::kReturnClicksFocusedControlistrue(ui/views/style/platform_style.h:56).ButtonController::OnKeyPressedcallsNotifyClickonkOnKeyPresswithout filtering repeated keys (ui/views/controls/button/button_controller.cc:86-109).
Potential Steps to Trigger
(Note: These are potential/suggested steps derived from static code analysis, as our tooling currently lacks the ability to execute live browser code dynamically.)
- Preconditions: Desktop Chrome (Windows or Linux), user is signed in to a Google account on the web (e.g. Gmail) under DICE/UNO but not signed in to the Chrome browser profile (
!identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSignin)), and an extension withidentityandidentity.emailpermissions and anoauth2manifest section is installed. - The extension opens or targets an
about:blankorchrome://newtabtab in the active window (which satisfiesSigninViewController::IsNTPTab,chrome/browser/ui/signin/signin_view_controller.cc:403-410). - The extension prompts the user to hold or repeatedly press the Enter key (e.g., through an in-extension game or UI interaction).
- The extension calls
chrome.identity.getAuthToken({interactive: true}). This call is accepted as interactive via recent user activity (identity_get_auth_token_function.cc:1103-1131). IdentityGetAuthTokenFunction::StartSigninFlow()detects DICE web-only sign-in and invokesIdentityAPI::MaybeShowChromeSigninDialog(), which routes toSigninViewController::MaybeShowChromeSigninDialogForExtensions().SigninViewControllersynchronously activates theabout:blanktab and shows the modal dialogChromeSigninChoiceForExtensionsPromptwith focus on the OK button.- The user’s held/repeated Enter keystrokes immediately trigger the focused OK button on key press, bypassing input protection.
ok_callbackexecutesidentity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount(account_id, signin::ConsentLevel::kSignin, signin_metrics::AccessPoint::kExtensions).IdentityGetAuthTokenFunction::OnPrimaryAccountChanged()automatically resumes token minting, and calls tochrome.identity.getProfileUserInfo({accountStatus: 'ANY'})return the user’s email address and GAIA ID without deliberate user consent.
Impact
- Unauthorized elevation of a web-only account to the primary browser account.
- Exposure of the user’s email address and GAIA ID to the requesting extension.
- Unlocking access to all web accounts for any installed extension querying
IdentityAPI::GetAccountsWithRefreshTokensForExtensions().
Suggested Fix
In chrome/browser/ui/signin/signin_view_controller.cc:958-974, update dialog_builder to override the default button, explicitly focus the cancel button, and enable input protection:
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kCancelButtonId);
dialog_builder.SetInternalName("ChromeSigninChoiceForExtensionsPrompt")
.SetTitle(title)
.AddParagraph(ui::DialogModelLabel(body))
.AddParagraph(ui::DialogModelLabel(l10n_util::GetStringUTF16(
IDS_EXTENSION_ASKS_IDENTITY_WHILE_SIGNED_IN_WEB_ONLY_BODY_PART_2)))
.AddOkButton(
base::BindOnce(std::move(ok_callback)),
ui::DialogModel::Button::Params().SetLabel(l10n_util::GetStringFUTF16(
IDS_PROFILES_DICE_WEB_ONLY_SIGNIN_BUTTON, continue_as_text)))
.AddCancelButton(base::DoNothing(),
ui::DialogModel::Button::Params()
.SetId(kCancelButtonId)
.SetLabel(l10n_util::GetStringUTF16(IDS_CANCEL)))
.OverrideDefaultButton(ui::mojom::DialogButton::kNone)
.SetInitiallyFocusedField(kCancelButtonId)
.SetEnableInputProtection(true)
.SetDialogDestroyingCallback(std::move(on_complete));
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
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.