CVE-2026-13895
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc |
modified |
Files Changed
chrome/browser/ui/views/autofill/popup/popup_row_view.ccchrome/browser/ui/views/autofill/popup/popup_row_view.hchrome/browser/ui/views/autofill/popup/popup_view_views.ccchrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
Patch
From cc3bca17f9eee3c1c431a2ac8b8d7fd657485662 Mon Sep 17 00:00:00 2001
From: Christoph Schwering <schwering@google.com>
Date: Fri, 22 May 2026 07:12:38 -0700
Subject: [PATCH] [Autofill] Block TAB key for 500 ms from accepting a suggestion
This CL blocks accepting a suggestion with the TAB key subject for
500 ms block, same as for the RETURN key, if
kAutofillPopupDontAcceptNonVisibleEnoughSuggestion is enabled.
Bug: 501770542
Change-Id: Ie8844a2627c754826755313469decc7f0c230eb8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858320
Reviewed-by: Bruno Braga <brunobraga@google.com>
Commit-Queue: Christoph Schwering <schwering@google.com>
Cr-Commit-Position: refs/heads/main@{#1634923}
---
diff --git a/chrome/browser/ui/views/autofill/popup/popup_row_view.cc b/chrome/browser/ui/views/autofill/popup/popup_row_view.cc
index eb06788b..333fb65 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_row_view.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_row_view.cc
@@ -306,20 +306,16 @@
}
if (event.IsOnlyLeftMouseButton() &&
- content_view_->HitTestPoint(event.location()) && controller_ &&
- IsViewVisibleEnough()) {
- controller_->AcceptSuggestion(
- line_number_, AutofillMetrics::SuggestionAcceptedMethod::kMouse);
+ content_view_->HitTestPoint(event.location())) {
+ Accept(AutofillMetrics::SuggestionAcceptedMethod::kMouse);
}
}
void PopupRowView::OnGestureEvent(ui::GestureEvent* event) {
switch (event->type()) {
case ui::EventType::kGestureTap:
- if (content_view_->HitTestPoint(event->location()) && controller_ &&
- IsViewVisibleEnough()) {
- controller_->AcceptSuggestion(
- line_number_, AutofillMetrics::SuggestionAcceptedMethod::kTap);
+ if (content_view_->HitTestPoint(event->location())) {
+ Accept(AutofillMetrics::SuggestionAcceptedMethod::kTap);
}
break;
default:
@@ -440,11 +436,8 @@
case ui::VKEY_RETURN: {
const bool kHasKeyModifierPressed =
event.GetModifiers() & blink::WebInputEvent::kKeyModifiers;
- if (*GetSelectedCell() == CellType::kContent && controller_ &&
- !kHasKeyModifierPressed && IsViewVisibleEnough()) {
- controller_->AcceptSuggestion(
- line_number_, AutofillMetrics::SuggestionAcceptedMethod::kKeyboard);
- return true;
+ if (*GetSelectedCell() == CellType::kContent && !kHasKeyModifierPressed) {
+ return Accept(AutofillMetrics::SuggestionAcceptedMethod::kKeyboard);
}
return false;
}
@@ -458,6 +451,15 @@
!controller_->GetSuggestionAt(line_number_).HasDeactivatedStyle();
}
+bool PopupRowView::Accept(
+ AutofillMetrics::SuggestionAcceptedMethod method) const {
+ if (controller_ && IsViewVisibleEnough()) {
+ controller_->AcceptSuggestion(line_number_, method);
+ return true;
+ }
+ return false;
+}
+
void PopupRowView::OnCellSelected(std::optional<CellType> type,
PopupCellSelectionSource source) {
selection_delegate_->SetSelectedCell(
diff --git a/chrome/browser/ui/views/autofill/popup/popup_row_view.h b/chrome/browser/ui/views/autofill/popup/popup_row_view.h
index c859aef..0be1d55 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_row_view.h
+++ b/chrome/browser/ui/views/autofill/popup/popup_row_view.h
@@ -14,6 +14,7 @@
#include "base/scoped_observation.h"
#include "chrome/browser/ui/autofill/next_idle_barrier.h"
#include "chrome/browser/ui/views/autofill/popup/popup_view_utils.h"
+#include "components/autofill/core/browser/metrics/autofill_metrics.h"
#include "components/input/native_web_keyboard_event.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/events/event_handler.h"
@@ -118,6 +119,9 @@
// Returns if the popup row is available for selection.
bool IsSelectable() const;
+ // Accepts the suggestion that corresponds to this view's line number.
+ bool Accept(AutofillMetrics::SuggestionAcceptedMethod method) const;
+
// Returns the view representing the content area of the row.
PopupRowContentView& GetContentView() { return *content_view_; }
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_views.cc b/chrome/browser/ui/views/autofill/popup/popup_view_views.cc
index 3fdbf1e9..fa0289f 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_views.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_views.cc
@@ -757,8 +757,7 @@
return false;
}
- controller_->AcceptSuggestion(index->first, accept_method);
- return true;
+ return GetPopupRowViewAt(index->first).Accept(accept_method);
}
bool PopupViewViews::RemoveSelectedCell() {
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
index 3588fe7..f0b0d757 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
@@ -16,7 +16,6 @@
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
-#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/time/time.h"
#include "build/build_config.h"
@@ -44,6 +43,7 @@
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/browser/ui/tabbed_pane_enums.h"
#include "components/autofill/core/common/aliases.h"
+#include "components/autofill/core/common/autofill_features.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/input/native_web_keyboard_event.h"
#include "components/strings/grit/components_strings.h"
@@ -1545,7 +1545,45 @@
EXPECT_FALSE(bnpl_footnote->IsSettingsLinkFocused());
}
-// Verify that pressing the tab key while the "Manage addresses..." entry is
+// Tests that accepting a suggestion with the TAB key is blocked for 500 ms
+// (AutofillSuggestionController::kIgnoreEarlyClicksOnSuggestionsDuration)
+// (crbug.com/501770542).
+TEST_F(PopupViewViewsTest, TabAcceptsSuggestionOnlyWhenRowVisibleLongEnough) {
+ MockFunction<void(std::string_view)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("No time passed."));
+ EXPECT_CALL(controller(),
+ AcceptSuggestion(
+ 0, AutofillMetrics::SuggestionAcceptedMethod::kKeyboard))
+ .Times(0);
+ EXPECT_CALL(check, Call("Insufficient time passed."));
+ EXPECT_CALL(controller(),
+ AcceptSuggestion(
+ 0, AutofillMetrics::SuggestionAcceptedMethod::kKeyboard))
+ .Times(0);
+ }
+
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(
+ features::kAutofillPopupDontAcceptNonVisibleEnoughSuggestion);
+ ON_CALL(controller(), IsViewVisibilityAcceptingThresholdEnabled())
+ .WillByDefault(Return(true));
+
+ CreateAndShowView({SuggestionType::kAddressEntry});
+ view().SetSelectedCell(CellIndex{0u, CellType::kContent},
+ PopupCellSelectionSource::kNonUserInput);
+ ASSERT_EQ(view().GetSelectedCell(),
+ std::make_optional<CellIndex>(0u, CellType::kContent));
+
+ check.Call("No time passed.");
+ SimulateKeyPress(ui::VKEY_TAB);
+ task_environment()->FastForwardBy(base::Milliseconds(499));
+ check.Call("Insufficient time passed.");
+ SimulateKeyPress(ui::VKEY_TAB);
+}
+
+// Verifies that pressing the tab key while the "Manage addresses..." entry is
// selected does not trigger "accepting" the entry (which would mean opening
// a tab with the autofill settings).
TEST_F(PopupViewViewsTest, NoAutofillOptionsTriggeredOnTabPressed) {
@@ -2890,7 +2928,7 @@
PopupViewViews::kAtMemoryPopupWidth);
// Allow Hide(kSearchBarFocusLost) which happens during teardown.
- testing::Mock::VerifyAndClearExpectations(&controller());
+ Mock::VerifyAndClearExpectations(&controller());
EXPECT_CALL(controller(), Hide(SuggestionHidingReason::kSearchBarFocusLost))
.Times(testing::AnyNumber());
Regression Test / PoC
diff --git a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
index 3588fe7..f0b0d757 100644
--- a/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
+++ b/chrome/browser/ui/views/autofill/popup/popup_view_views_unittest.cc
@@ -16,7 +16,6 @@
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
-#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/time/time.h"
#include "build/build_config.h"
@@ -44,6 +43,7 @@
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/browser/ui/tabbed_pane_enums.h"
#include "components/autofill/core/common/aliases.h"
+#include "components/autofill/core/common/autofill_features.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/input/native_web_keyboard_event.h"
#include "components/strings/grit/components_strings.h"
@@ -1545,7 +1545,45 @@
EXPECT_FALSE(bnpl_footnote->IsSettingsLinkFocused());
}
-// Verify that pressing the tab key while the "Manage addresses..." entry is
+// Tests that accepting a suggestion with the TAB key is blocked for 500 ms
+// (AutofillSuggestionController::kIgnoreEarlyClicksOnSuggestionsDuration)
+// (crbug.com/501770542).
+TEST_F(PopupViewViewsTest, TabAcceptsSuggestionOnlyWhenRowVisibleLongEnough) {
+ MockFunction<void(std::string_view)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("No time passed."));
+ EXPECT_CALL(controller(),
+ AcceptSuggestion(
+ 0, AutofillMetrics::SuggestionAcceptedMethod::kKeyboard))
+ .Times(0);
+ EXPECT_CALL(check, Call("Insufficient time passed."));
+ EXPECT_CALL(controller(),
+ AcceptSuggestion(
+ 0, AutofillMetrics::SuggestionAcceptedMethod::kKeyboard))
+ .Times(0);
+ }
+
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(
+ features::kAutofillPopupDontAcceptNonVisibleEnoughSuggestion);
+ ON_CALL(controller(), IsViewVisibilityAcceptingThresholdEnabled())
+ .WillByDefault(Return(true));
+
+ CreateAndShowView({SuggestionType::kAddressEntry});
+ view().SetSelectedCell(CellIndex{0u, CellType::kContent},
+ PopupCellSelectionSource::kNonUserInput);
+ ASSERT_EQ(view().GetSelectedCell(),
+ std::make_optional<CellIndex>(0u, CellType::kContent));
+
+ check.Call("No time passed.");
+ SimulateKeyPress(ui::VKEY_TAB);
+ task_environment()->FastForwardBy(base::Milliseconds(499));
+ check.Call("Insufficient time passed.");
+ SimulateKeyPress(ui::VKEY_TAB);
+}
+
+// Verifies that pressing the tab key while the "Manage addresses..." entry is
// selected does not trigger "accepting" the entry (which would mean opening
// a tab with the autofill settings).
TEST_F(PopupViewViewsTest, NoAutofillOptionsTriggeredOnTabPressed) {
@@ -2890,7 +2928,7 @@
PopupViewViews::kAtMemoryPopupWidth);
// Allow Hide(kSearchBarFocusLost) which happens during teardown.
- testing::Mock::VerifyAndClearExpectations(&controller());
+ Mock::VerifyAndClearExpectations(&controller());
EXPECT_CALL(controller(), Hide(SuggestionHidingReason::kSearchBarFocusLost))
.Times(testing::AnyNumber());
Original Bug Report
Autofill: Potential bypass of dwell-time security check via TAB key acceptance
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 Autofill popup implements a 500ms dwell-time security check on individual rows to prevent UI redress attacks when a user accepts a suggestion. However, this per-row check is bypassed when accepting a suggestion using the TAB key, allowing an attacker to swap suggestions dynamically and have them accepted immediately if the overall popup has been open long enough.
Affected files:
chrome/browser/ui/views/autofill/popup/popup_view_views.ccchrome/browser/ui/views/autofill/popup/popup_row_view.ccchrome/browser/ui/autofill/autofill_popup_controller_impl.cc
Estimated timestamp from git blame: 2025-08-11
Summary
To protect users from UI redress attacks (like clickjacking or suggestion swapping), the Autofill popup implements a security hardening feature (kAutofillPopupDontAcceptNonVisibleEnoughSuggestion). This feature enforces a 500ms dwell-time requirement before a suggestion can be accepted.
The protection is implemented in two layers:
- A global popup-level barrier in
AutofillPopupControllerImplthat ensures the popup itself has been visible for 500ms. - A per-row barrier in
PopupRowView(IsViewVisibleEnough()) that ensures a specific row has been visible for 500ms.
While mouse clicks, gestures, and the ENTER key correctly enforce the per-row barrier, the TAB key bypasses it entirely. If an attacker dynamically updates the suggestions while the popup is open, the global barrier is not reset. The new rows get fresh per-row barriers, but because TAB ignores them, an attacker can trick a user navigating via keyboard into immediately accepting a newly swapped, sensitive suggestion.
Technical Analysis
In chrome/browser/ui/views/autofill/popup/popup_row_view.cc, interactions such as mouse release, gesture tap, and VKEY_RETURN all gate suggestion acceptance on IsViewVisibleEnough():
// popup_row_view.cc : HandleKeyPressEvent
case ui::VKEY_RETURN: {
if (*GetSelectedCell() == CellType::kContent && controller_ &&
!kHasKeyModifierPressed && IsViewVisibleEnough()) {
controller_->AcceptSuggestion(...);
return true;
}
}
However, the TAB key is handled in the parent view, PopupViewViews::HandleKeyPressEvent (chrome/browser/ui/views/autofill/popup/popup_view_views.cc):
case ui::VKEY_TAB:
if (!kHasNonShiftModifier) {
AcceptSelectedContentOrCreditCardCell(AutofillMetrics::SuggestionAcceptedMethod::kKeyboard);
}
return false;
AcceptSelectedContentOrCreditCardCell verifies that a cell is selected and then directly calls controller_->AcceptSuggestion(). It does not check IsViewVisibleEnough() or any other row-level visibility state.
When controller_->AcceptSuggestion() is called, it only checks the global popup-level barrier_for_accepting_. When a web page dynamically updates <datalist> options, AutofillPopupControllerImpl::UpdateDataListValues updates the suggestions and recreates the row views, but it does not reset this global barrier.
Potential Exploitation Steps
An attacker can exploit this inconsistency using the following suggested steps (note: this sequence relies on code analysis; we do not have a working PoC):
- The attacker creates a form with an
<input>field linked to a JavaScript-controlled<datalist>. - The user interacts with the input, triggering the Autofill popup.
- The attacker’s script waits for >500ms, satisfying the global
AutofillPopupControllerImplbarrier. - The user begins a fast, natural keyboard navigation sequence (e.g.,
Down Arrow->TAB) to select the first suggestion and move to the next field. - Just before the user presses
Down Arrow, the attacker’s script updates the<datalist>, placing a sensitive autofill profile at the top. - The update recreates the row views. The new top row gets a fresh 500ms per-row barrier to protect against immediate acceptance.
- The user’s
Down Arrowkey press selects the new top row. - The user’s
TABkey press triggersAcceptSelectedContentOrCreditCardCell. The per-row barrier is bypassed, and the global barrier has already elapsed. - The sensitive suggestion is accepted immediately, filling the form for the attacker to exfiltrate.
Suggested Fix
Ensure that the TAB key enforces the same security checks as the ENTER key.
One approach is to modify PopupViewViews::AcceptSelectedContentOrCreditCardCell to verify the selected row’s visibility before calling the controller:
std::optional<CellIndex> index = GetSelectedCell();
// ... existing checks ...
PopupRowView& row_view = GetPopupRowViewAt(index->first);
if (!row_view.IsViewVisibleEnough()) {
return false;
}
controller_->AcceptSuggestion(index->first, accept_method);
Alternatively, VKEY_TAB handling could be delegated to PopupRowView::HandleKeyPressEvent similarly to VKEY_RETURN, ensuring unified acceptance logic.
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.