Chrome · Autofill
CVE-2026-79014
Race in Autofill
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ChromeAutofillClientTestWithMockWindowchrome/browser/ui/autofill/chrome_autofill_client_unittest.cc |
modified | |
ChromeAutofillClientTestWithMockWindowchrome/browser/ui/autofill/chrome_autofill_client_unittest.cc |
modified | |
GetQueriedFormAndFieldcomponents/autofill/core/browser/ui/autofill_external_delegate.cc |
modified | |
PasswordManagerDrivercomponents/autofill/core/browser/ui/autofill_suggestion_delegate.h |
modified |
Files Changed
chrome/browser/ui/autofill/chrome_autofill_client.ccchrome/browser/ui/autofill/chrome_autofill_client.hchrome/browser/ui/autofill/chrome_autofill_client_unittest.cccomponents/autofill/core/browser/ui/autofill_external_delegate.cccomponents/autofill/core/browser/ui/autofill_external_delegate.hcomponents/autofill/core/browser/ui/autofill_suggestion_delegate.hcomponents/autofill/core/browser/ui/mock_autofill_suggestion_delegate.cc
Patch
From db5774f14bba682c4f6db634928d48ab6746a98e Mon Sep 17 00:00:00 2001
From: Bruno Braga <brunobraga@google.com>
Date: Thu, 13 Aug 2026 02:07:12 -0700
Subject: [PATCH] [Autofill] Verify queried field before showing suggestions popup
Between posting ShowAutofillSuggestionsImpl to the UI task runner and
its execution, an attacker renderer could spam AskForValuesToFill IPCs
for another field. This mutates the delegate's active query state (e.g.
last_query_ in AutofillExternalDelegate) prior to UI popup creation,
leading to a desync where the displayed popup was generated for frame A
but selecting a suggestion queries last_query_ holding frame B's field
ID.
This CL introduces GetQueriedFieldId() to AutofillSuggestionDelegate and
snapshots the expected field ID inside ShowAutofillSuggestions(). Upon
executing ShowAutofillSuggestionsImpl(), we verify that the delegate's
current queried field ID still matches expected_field_id before showing
the popup. If it changed in the interim, display is aborted.
Bug: 536568319
Change-Id: I7d879924e0f1593048ec1a79fa58f344e854a670
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8233280
Reviewed-by: Viktor Semeniuk <vsemeniuk@google.com>
Reviewed-by: Jan Keitel <jkeitel@google.com>
Commit-Queue: Bruno Braga <brunobraga@google.com>
Cr-Commit-Position: refs/heads/main@{#1678702}
---
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.cc b/chrome/browser/ui/autofill/chrome_autofill_client.cc
index 4dee2cb0..d9cdce6 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.cc
@@ -877,13 +877,16 @@
// guarantees the IPH will be hidden by the time the Autofill Popup will
// attempt to open. This works because the tasks of hiding the IPH and showing
// the Autofill Popup are posted on the same thread (UI thread).
+ const FieldGlobalId expected_field_id =
+ delegate ? delegate->GetQueriedFieldId() : FieldGlobalId();
+
const SuggestionUiSessionId session_id =
AutofillSuggestionController::GenerateSuggestionUiSessionId();
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ChromeAutofillClient::ShowAutofillSuggestionsImpl,
weak_ptr_factory_.GetWeakPtr(), session_id, open_args,
- delegate));
+ delegate, expected_field_id));
return session_id;
}
@@ -1376,7 +1379,15 @@
void ChromeAutofillClient::ShowAutofillSuggestionsImpl(
SuggestionUiSessionId session_id,
const PopupOpenArgs& open_args,
- base::WeakPtr<AutofillSuggestionDelegate> delegate) {
+ base::WeakPtr<AutofillSuggestionDelegate> delegate,
+ FieldGlobalId expected_field_id) {
+ if (expected_field_id &&
+ (!delegate || delegate->GetQueriedFieldId() != expected_field_id) &&
+ base::FeatureList::IsEnabled(
+ features::kAutofillCheckTriggeringFieldDoesNotChangeDuringFilling)) {
+ return;
+ }
+
// Convert element_bounds to be in screen space.
const gfx::Rect client_area = web_contents()->GetContainerBounds();
const gfx::RectF element_bounds_in_screen_space =
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.h b/chrome/browser/ui/autofill/chrome_autofill_client.h
index b1a34704..e7a20d0 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.h
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.h
@@ -393,7 +393,8 @@
void ShowAutofillSuggestionsImpl(
SuggestionUiSessionId session_id,
const PopupOpenArgs& open_args,
- base::WeakPtr<AutofillSuggestionDelegate> delegate);
+ base::WeakPtr<AutofillSuggestionDelegate> delegate,
+ FieldGlobalId expected_field_id);
// Called when an actor task is created or an existing one changes state. It
// may be called for actors unrelated to the current tab. If an update is
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
index f6e8e9b..9914e79c 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
@@ -630,6 +630,58 @@
testing::Mock::VerifyAndClearExpectations(autofill_field_promo_controller());
}
+TEST_F(ChromeAutofillClientTest,
+ ShowAutofillSuggestions_AbortsIfQueriedFieldChanges) {
+ auto delegate = std::make_unique<MockAutofillSuggestionDelegate>();
+
+ FieldGlobalId field1(LocalFrameToken(base::UnguessableToken::Create()),
+ FieldRendererId(1));
+ FieldGlobalId field2(LocalFrameToken(base::UnguessableToken::Create()),
+ FieldRendererId(2));
+
+ ON_CALL(*delegate, GetQueriedFieldId).WillByDefault(Return(field1));
+
+ // Because ShowAutofillSuggestionsImpl() early-returns upon detecting a
+ // queried field mismatch (field2 != field1), it aborts before creating or
+ // showing an AutofillSuggestionController. Thus, no popup session is
+ // created and delegate->OnSuggestionsHidden() must not be called (0 times).
+ EXPECT_CALL(*delegate, OnSuggestionsHidden).Times(0);
+
+ client()->ShowAutofillSuggestions(AutofillClient::PopupOpenArgs(),
+ delegate->GetWeakPtr());
+
+ ON_CALL(*delegate, GetQueriedFieldId).WillByDefault(Return(field2));
+
+ {
+ base::RunLoop run_loop;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, run_loop.QuitClosure());
+ run_loop.Run();
+ }
+
+ testing::Mock::VerifyAndClearExpectations(delegate.get());
+
+ // When the queried field ID matches (field2 == field2),
+ // ShowAutofillSuggestionsImpl() passes the guard check and proceeds to
+ // create AutofillSuggestionController and call Show(). In this headless unit
+ // test environment without window focus, Show() immediately dismisses the
+ // popup with kNoFrameHasFocus, triggering delegate->OnSuggestionsHidden()
+ // exactly once.
+ EXPECT_CALL(*delegate,
+ OnSuggestionsHidden(SuggestionHidingReason::kNoFrameHasFocus))
+ .Times(1);
+
+ client()->ShowAutofillSuggestions(AutofillClient::PopupOpenArgs(),
+ delegate->GetWeakPtr());
+
+ {
+ base::RunLoop run_loop;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, run_loop.QuitClosure());
+ run_loop.Run();
+ }
+}
+
class ChromeAutofillClientTestWithMockWindow : public ChromeAutofillClientTest {
public:
ChromeAutofillClientTestWithMockWindow() {
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.cc b/components/autofill/core/browser/ui/autofill_external_delegate.cc
index 40c289f..e8cb74b 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.cc
@@ -434,6 +434,10 @@
return GetQueriedFormAndField().second;
}
+FieldGlobalId AutofillExternalDelegate::GetQueriedFieldId() const {
+ return last_query_.field_id;
+}
+
std::pair<const FormStructure*, const AutofillField*>
AutofillExternalDelegate::GetQueriedFormAndField() const {
auto [form, field] =
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.h b/components/autofill/core/browser/ui/autofill_external_delegate.h
index bd4c2c0..e39cb45 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.h
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.h
@@ -97,6 +97,8 @@
void OnTabSelected(TabbedPaneTabType tab_type) override;
+ FieldGlobalId GetQueriedFieldId() const override;
+
// Called when the renderer posts an Autofill query to the browser. We might
// not want to display the warning if a website has disabled Autocomplete
// because they have their own popup, and showing our popup on to of theirs
diff --git a/components/autofill/core/browser/ui/autofill_suggestion_delegate.h b/components/autofill/core/browser/ui/autofill_suggestion_delegate.h
index 43bc802..0402ce6d 100644
--- a/components/autofill/core/browser/ui/autofill_suggestion_delegate.h
+++ b/components/autofill/core/browser/ui/autofill_suggestion_delegate.h
@@ -14,6 +14,7 @@
#include "components/autofill/core/browser/suggestions/suggestion.h"
#include "components/autofill/core/browser/suggestions/suggestion_hiding_reason.h"
#include "components/autofill/core/browser/ui/tabbed_pane_enums.h"
+#include "components/autofill/core/common/unique_ids.h"
namespace password_manager {
class PasswordManagerDriver;
@@ -111,6 +112,9 @@
// Called when `tab_type` is opened in the tabbed pane config of the autofill
// dropdown.
virtual void OnTabSelected(TabbedPaneTabType tab_type) = 0;
+
+ // Returns the global ID of the field for which suggestions are being queried.
+ virtual FieldGlobalId GetQueriedFieldId() const = 0;
};
} // namespace autofill
diff --git a/components/autofill/core/browser/ui/mock_autofill_suggestion_delegate.cc b/components/autofill/core/browser/ui/mock_autofill_suggestion_delegate.cc
index 0105981c..ed50187 100644
--- a/components/autofill/core/browser/ui/mock_autofill_suggestion_delegate.cc
+++ b/components/autofill/core/browser/ui/mock_autofill_suggestion_delegate.cc
@@ -6,7 +6,10 @@
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
index f6e8e9b..9914e79c 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
@@ -630,6 +630,58 @@
testing::Mock::VerifyAndClearExpectations(autofill_field_promo_controller());
}
+TEST_F(ChromeAutofillClientTest,
+ ShowAutofillSuggestions_AbortsIfQueriedFieldChanges) {
+ auto delegate = std::make_unique<MockAutofillSuggestionDelegate>();
+
+ FieldGlobalId field1(LocalFrameToken(base::UnguessableToken::Create()),
+ FieldRendererId(1));
+ FieldGlobalId field2(LocalFrameToken(base::UnguessableToken::Create()),
+ FieldRendererId(2));
+
+ ON_CALL(*delegate, GetQueriedFieldId).WillByDefault(Return(field1));
+
+ // Because ShowAutofillSuggestionsImpl() early-returns upon detecting a
+ // queried field mismatch (field2 != field1), it aborts before creating or
+ // showing an AutofillSuggestionController. Thus, no popup session is
+ // created and delegate->OnSuggestionsHidden() must not be called (0 times).
+ EXPECT_CALL(*delegate, OnSuggestionsHidden).Times(0);
+
+ client()->ShowAutofillSuggestions(AutofillClient::PopupOpenArgs(),
+ delegate->GetWeakPtr());
+
+ ON_CALL(*delegate, GetQueriedFieldId).WillByDefault(Return(field2));
+
+ {
+ base::RunLoop run_loop;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, run_loop.QuitClosure());
+ run_loop.Run();
+ }
+
+ testing::Mock::VerifyAndClearExpectations(delegate.get());
+
+ // When the queried field ID matches (field2 == field2),
+ // ShowAutofillSuggestionsImpl() passes the guard check and proceeds to
+ // create AutofillSuggestionController and call Show(). In this headless unit
+ // test environment without window focus, Show() immediately dismisses the
+ // popup with kNoFrameHasFocus, triggering delegate->OnSuggestionsHidden()
+ // exactly once.
+ EXPECT_CALL(*delegate,
+ OnSuggestionsHidden(SuggestionHidingReason::kNoFrameHasFocus))
+ .Times(1);
+
+ client()->ShowAutofillSuggestions(AutofillClient::PopupOpenArgs(),
+ delegate->GetWeakPtr());
+
+ {
+ base::RunLoop run_loop;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, run_loop.QuitClosure());
+ run_loop.Run();
+ }
+}
+
class ChromeAutofillClientTestWithMockWindow : public ChromeAutofillClientTest {
public:
ChromeAutofillClientTestWithMockWindow() {
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page