CVE-2026-79082
Overview
Background
- `AutofillExternalDelegate` (AED)
- the browser-side object that receives an Autofill query for a focused form field, shows the suggestion popup (addresses, payment cards and so on) through the
AutofillClient, and acts on the suggestion the user accepts. - `last_query_`
- AED state recording the
form_id(FormGlobalId) andfield_id(FieldGlobalId) of the most recent query, plus the field’s datalist options; it determines which form and field the delegate treats as the current target. - `HideSuggestions` / `SuggestionHidingReason::kStaleData`
- the client call that closes the open suggestion popup, here with a reason saying the displayed data no longer matches the current context.
Root Cause Analysis
Before the patch, AutofillExternalDelegate::OnQuery overwrote last_query_ with the new form and field IDs without closing a suggestion popup that was already open for a different form or field. A popup built for one query target could therefore stay on screen while the delegate’s notion of the target had silently moved elsewhere, so the visible suggestions and the field they would act on could disagree, which the commit title calls “staleness”.
The patch adds a check at the top of OnQuery: if a previous query exists (last_query_.form_id or last_query_.field_id set) and either ID differs from the incoming form.global_id() or field.global_id(), it calls manager_->client().HideSuggestions(SuggestionHidingReason::kStaleData, std::nullopt) before updating last_query_. A re-query of the same form and field leaves the popup alone, as the new unit test checks. This turns a potential mismatch between what the user sees and approves and what Autofill targets into an invariant: an open popup always belongs to last_query_. The diff does not show the downstream fill or accept path, so the exact misuse of the stale popup is not shown here.
OnQuery changed the delegate’s target (last_query_) without invalidating UI that was authorised for the previous target. The fix hides the existing popup whenever the queried form or field identity changes.Attack Path
- Victim opens Autofill suggestions The user focuses a field on a page and Chrome shows an Autofill popup (for example with stored address or payment data) built for that form and field.
- Page shifts the query target
While the popup is open, page script causes a new Autofill query for a different form or field, for example by moving focus or changing the DOM. The concrete trigger is
(inferred), as the patch only shows thatOnQuerycan run with a differentform.global_id()orfield.global_id(). - Stale popup remains
Before the fix,
OnQueryupdatedlast_query_to the new target and left the old popup visible, so the shown suggestions no longer matched the delegate’s current target. - User acceptance applied out of context
If the user then selected a suggestion, the action could be associated with a form or field the user did not intend, for example one the page controls more directly, which could expose Autofill data to it
(inferred).
Impact Assessment
(inferred); the advisory states only “Incorrect authorization in Transactions Platform” with no in-the-wild status.Files Changed
components/autofill/core/browser/ui/autofill_external_delegate_unittest.cccomponents/autofill/core/browser/ui/autofill_external_delegate.cc
Audit Directions
- Target state updated under open UILook for other Autofill and password-manager delegates (
PasswordAutofillManager, payments bubbles,AutofillPopupControllerImpl) where focus or query handlers update their current-field state while a popup or bubble from the previous field stays open. - Accept handlers trusting mutable contextCheck that suggestion-acceptance paths re-validate that the popup’s originating form and field still match the delegate’s current
last_query_, and do not just read whatever target is current at accept time. - Identity comparison completenessWhere staleness is detected by comparing
FormGlobalIdandFieldGlobalId, confirm that comparisons cover frame tokens and renderer IDs, so a same-shaped form in another frame is not treated as the same target.
Patch
From f8b712bd29e778755681cc75245a65140b8f4107 Mon Sep 17 00:00:00 2001
From: Jihad Hanna <jihadghanna@google.com>
Date: Fri, 17 Jul 2026 11:55:13 -0700
Subject: [PATCH] Hide Autofill popup on AED::OnQuery() to avoid staleness
Fixed: 524698525, 521951328
Change-Id: I09ae8cd3a11f1d630ee4e8bab278b1342ec822ed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8116769
Commit-Queue: Jihad Hanna <jihadghanna@google.com>
Auto-Submit: Jihad Hanna <jihadghanna@google.com>
Reviewed-by: Jan Keitel <jkeitel@google.com>
Cr-Commit-Position: refs/heads/main@{#1664080}
---
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.cc b/components/autofill/core/browser/ui/autofill_external_delegate.cc
index f3e996d..b67eaae 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.cc
@@ -379,6 +379,13 @@
const FormFieldData& field,
const gfx::Rect& caret_bounds,
AutofillSuggestionTriggerSource trigger_source) {
+ if ((last_query_.form_id || last_query_.field_id) &&
+ (last_query_.form_id != form.global_id() ||
+ last_query_.field_id != field.global_id())) {
+ manager_->client().HideSuggestions(SuggestionHidingReason::kStaleData,
+ /*product=*/std::nullopt);
+ }
+
last_query_ = {.form_id = form.global_id(),
.field_id = field.global_id(),
.field_datalist_options = field.datalist_options()};
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
index fdb281a..c43c82da 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
@@ -4278,6 +4278,29 @@
SuggestionPosition{.multi_index = {0}});
}
+// Tests that OnQuery() hides any open suggestions with kStaleData only when the
+// queried form or field changes from the previous query.
+TEST_F(AutofillExternalDelegateTest,
+ ExternalDelegateHidesSuggestionsWhenQueryTargetChanges) {
+ IssueOnQuery();
+
+ // Re-querying the exact same form and field should not hide suggestions.
+ EXPECT_CALL(autofill_client(), HideSuggestions).Times(0);
+ external_delegate().OnQuery(queried_form(), queried_field(), gfx::Rect(),
+ kDefaultSuggestionTriggerSource);
+
+ // Querying a different field (or form) should hide suggestions with
+ // kStaleData.
+ EXPECT_CALL(
+ autofill_client(),
+ HideSuggestions(SuggestionHidingReason::kStaleData, Eq(std::nullopt)));
+ FormData new_form = test::GetFormData(
+ {.fields = {{.role = NAME_FULL, .autocomplete_attribute = "name"}}});
+ autofill_manager().OnFormsSeen({new_form}, {});
+ external_delegate().OnQuery(new_form, new_form.fields()[0], gfx::Rect(),
+ kDefaultSuggestionTriggerSource);
+}
+
} // namespace
} // namespace autofill
Regression Test / PoC
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
index fdb281a..c43c82da 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
@@ -4278,6 +4278,29 @@
SuggestionPosition{.multi_index = {0}});
}
+// Tests that OnQuery() hides any open suggestions with kStaleData only when the
+// queried form or field changes from the previous query.
+TEST_F(AutofillExternalDelegateTest,
+ ExternalDelegateHidesSuggestionsWhenQueryTargetChanges) {
+ IssueOnQuery();
+
+ // Re-querying the exact same form and field should not hide suggestions.
+ EXPECT_CALL(autofill_client(), HideSuggestions).Times(0);
+ external_delegate().OnQuery(queried_form(), queried_field(), gfx::Rect(),
+ kDefaultSuggestionTriggerSource);
+
+ // Querying a different field (or form) should hide suggestions with
+ // kStaleData.
+ EXPECT_CALL(
+ autofill_client(),
+ HideSuggestions(SuggestionHidingReason::kStaleData, Eq(std::nullopt)));
+ FormData new_form = test::GetFormData(
+ {.fields = {{.role = NAME_FULL, .autocomplete_attribute = "name"}}});
+ autofill_manager().OnFormsSeen({new_form}, {});
+ external_delegate().OnQuery(new_form, new_form.fields()[0], gfx::Rect(),
+ kDefaultSuggestionTriggerSource);
+}
+
} // namespace
} // namespace autofill
Original Bug Report
Potential cross-origin Autofill bypass and data leak via non-focusable field desynchronization
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: A logic flaw in AutofillExternalDelegate potentially allows a compromised main frame to bypass cross-origin IsSafeToFill boundaries and exfiltrate sensitive payment card or address profile data. By triggering an Autofill suggestions query on a non-focusable field while a legitimate suggestions popup is already visible, the attacker can desynchronize the internal Autofill state. Consequently, when the user selects a suggestion, the decrypted sensitive data is filled into the attacker-controlled origin’s fields.
Affected files:
components/autofill/core/browser/ui/autofill_external_delegate.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.ccchrome/browser/ui/autofill/autofill_popup_controller_impl.cc
Estimated timestamp from git blame: 2026-04-13
Description
A logical vulnerability exists in the browser’s Autofill suggestion pipeline. Specifically, AutofillExternalDelegate::OnQuery() unconditionally updates its active state (query_form_ and query_field_) before checking if the queried field is eligible for suggestions.
If the field is subsequently determined to be non-focusable inside AttemptToDisplayAutofillSuggestions(), the browser triggers an early return without hiding the currently displayed popup. This leads to a permanent state desynchronization where a previously opened, legitimate credit card or address suggestions popup remains visible to the user, but the browser’s active query_field_ is redirected to the attacker’s origin.
Because of this desynchronization, when the user selects a suggestion from the popup, the browser routes the decrypted data (such as credit card PAN or CVC) to the attacker-controlled fields under the assumption that the trigger origin matches the target origin (IsSafeToFill boundary check passes).
Impact
An attacker who has compromised the renderer process of a main frame (origin https://A) that embeds a legitimate checkout frame (origin https://M) could potentially exfiltrate full decrypted credit card numbers (PAN), expiration dates, and CVCs. The attack requires only a single normal user click on a legitimately triggered Autofill popup, making it highly reliable and indistinguishable from normal user behavior.
Potential Step-by-Step Attack Scenario
Note: Our analysis is based on static and dynamic code review; our tooling does not currently have the capability to run a live exploit in a running instance of Chrome.
- A compromised main frame
https://Aembeds a legitimate cross-origin payment iframehttps://M. - The user interacts with the credit card number input field in the checkout iframe
https://M. - Renderer
Mtriggers a legitimate Autofill query. The browser’s rootBrowserAutofillManagerinvokesAutofillExternalDelegate::OnQuery(), setting:query_form_= the flattened form structurequery_field_= the focused field inhttps://M
- The browser displays the legitimate credit card suggestions popup over
M’s field in the UI. - The compromised root frame
https://Aprogrammatically sends a forgedmojom::AutofillDriver::AskForValuesToFillIPC for a hidden fieldfield_Ainside its own frame, explicitly settingfield_A.is_focusable = falseandfield_A.datalist_options = {}. - In the browser process,
BrowserAutofillManager::OnAskForValuesToFillImplis invoked and callsexternal_delegate_->OnQuery(form, field_A, ...). OnQuery()unconditionally overwrites the active delegate state:query_field_ = field_A(origin =https://A,is_focusable = false).- Because the datalist options list is empty,
UpdateAutofillDataListValuesdoes not dismiss or hide the existing credit card suggestions popup. - When suggestions are returned for
field_A, the delegate executesAttemptToDisplayAutofillSuggestions()and hits the focusability guard:The method immediately returns without calling// components/autofill/core/browser/ui/autofill_external_delegate.cc:414 if (!query_field_.is_focusable() || !manager_->driver().CanShowAutofillUi()) { return; }HideSuggestions(), leaving the legitimate credit card popup showing on screen. - The user clicks a card suggestion in the still-visible popup.
- The click is handled by
AutofillPopupControllerImpl::AcceptSuggestion()and routed todelegate_->DidAcceptSuggestion(), which retrieves the activequery_field_(which is nowfield_Aof originhttps://A). FormFiller::FillOrPreviewForm()is invoked. During safety verification,AutofillDriverRouter::IsSafeToFill()evaluates:Since both// components/autofill/core/browser/foundations/autofill_driver_router.cc:119 return field.origin() == trigger_origin || ...field(the target hidden field) andtrigger_origin(the overwrittenquery_field_origin) belong tohttps://A, the safety check evaluates totrue(safe).- The fully decrypted credit card credentials (PAN and CVC) are filled into frame
A’s hidden fields viaApplyFormActionand exfiltrated by the attacker’s renderer script.
Suggested Fix
To remediate this logical vulnerability, we recommend ensuring that the suggestions popup is explicitly dismissed if a query target is updated to an ineligible or non-focusable field.
Specifically, in AutofillExternalDelegate::AttemptToDisplayAutofillSuggestions() (at components/autofill/core/browser/ui/autofill_external_delegate.cc:414), instead of a bare return, the delegate should hide any open popup first:
if (!query_field_.is_focusable() || !manager_->driver().CanShowAutofillUi()) {
manager_->client().HideSuggestions(SuggestionHidingReason::kNoSuggestions,
/*product=*/std::nullopt);
return;
}
Alternatively, validation checks (such as focusability) should be performed before updating query_form_ and query_field_ inside BrowserAutofillManager::OnAskForValuesToFillImpl() or prior to dispatching OnQuery() to the delegate.
Evaluated with Chrome root at commit: 70c6813870b6701fa16670076bf633ee6c3a439f
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.