CVE-2026-95384
Overview
Background
- `AutofillExternalDelegate`
- the browser-side object that mediates between a shown Autofill popup and the
BrowserAutofillManager, holding per-interaction state such aslast_query_andtrigger_source_. - `last_query_`
- a member struct on the delegate recording the
FormGlobalIdandFieldGlobalIdof the field currently being queried, which is overwritten each time a new field triggers Autofill. - `base::BindOnce`
- a Chromium binder that captures its arguments at bind time, so a value passed directly is snapshotted while a captured
this/WeakPtris dereferenced only when the callback later runs. - `AutofillTriggerSource`
- an enum describing what user action initiated a fill (for example a popup selection), used to attribute the resulting form fill.
Root Cause Analysis
When a user accepted a suggestion that required an asynchronous fetch (an AutofillAI EntityInstance fetch, a BNPL credit-card decision, or an identity-credential acceptance), the completion callbacks captured a WeakPtr to the AutofillExternalDelegate and, only when the async work finished, read delegate->last_query_.form_id, delegate->last_query_.field_id, and delegate->trigger_source_ to drive FillOrPreviewForm. Because last_query_ and trigger_source_ are mutated on every subsequent Autofill interaction, nothing guaranteed they still described the field the user had actually acted on when the callback ran, violating the invariant that a fill targets the form and field the accepted suggestion was displayed for. If the page or user caused a new query to overwrite that state during the async window, the delayed callback would fill the wrong field/form with the fetched value.
The fix binds the FormGlobalId, FieldGlobalId, and derived AutofillTriggerSource by value into the callbacks at the moment of interaction and routes through a BrowserAutofillManager WeakPtr, so the target is snapshotted and immune to later state mutation. This closes the interval between interacting with a suggestion and executing its acceptance, though the CL notes the show-to-interact interval remains out of scope.
last_query_ and trigger_source_) lazily inside an async completion callback instead of capturing it at bind time; the fix snapshots the form id, field id, and trigger source as bound arguments so the fill always targets the field the user actually accepted.Attack Path
- Trigger an async-fetch suggestion
The user (or an attacker-influenced page) focuses a field and accepts a suggestion whose acceptance requires asynchronous work, causing
base::BindOnceto capture aWeakPtr<AutofillExternalDelegate>bound to readlast_query_later. - Mutate query state during the async window
Before the fetch/decision resolves, a new Autofill query on a different field overwrites
last_query_.form_id,last_query_.field_id, andtrigger_source_on the same delegate.(inferred) - Callback fills the stale target
The async callback completes and calls
FillOrPreviewFormusing the now-updatedlast_query_, directing the fetched credit card, entity instance, or identity-credential value at a form/field other than the one the user accepted for.(inferred) - Sensitive data lands in the wrong field
The mismatched fill can place payment or identity data into an unintended field, which an attacker-controlled form could be positioned to capture.
(inferred)
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/autofill/core/browser/ui/autofill_external_delegate.cc |
modified | |
switchcomponents/autofill/core/browser/ui/autofill_external_delegate.cc |
modified | |
AddressDataManagercomponents/autofill/core/browser/ui/autofill_external_delegate.h |
modified | |
AutofillDrivercomponents/autofill/core/browser/ui/autofill_external_delegate.h |
modified | |
BrowserAutofillManagercomponents/autofill/core/browser/ui/autofill_external_delegate.h |
modified | |
CreditCardcomponents/autofill/core/browser/ui/autofill_external_delegate.h |
modified | |
FormStructurecomponents/autofill/core/browser/ui/autofill_external_delegate.h |
modified |
Files Changed
components/autofill/core/browser/ui/autofill_external_delegate.cccomponents/autofill/core/browser/ui/autofill_external_delegate.h
Audit Directions
- Deferred state reads in async callbacksAudit other Autofill callbacks that capture a delegate/manager
WeakPtrand readlast_query_,trigger_source_, or similar per-interaction members at completion time rather than binding them as arguments. - Cross-query mutation windowsLook for members overwritten on every new query or focus event that are later consumed by long-running async operations (fetches, reauth, modal confirmations) without a captured snapshot.
- WeakPtr survives but context changesFlag patterns where a
WeakPtrliveness check is treated as sufficient for correctness while the object’s fields may have been mutated between bind and invocation.
Patch
From 1d6c6b5c9f9bd7887816816f0b95361205f6f7b5 Mon Sep 17 00:00:00 2001
From: Jihad Hanna <jihadghanna@google.com>
Date: Tue, 23 Jun 2026 01:51:53 -0700
Subject: [PATCH] Ensure state consistency in async callbacks after accepting suggestions
This CL binds the query form and field ID to async tasks happening upon
interacting with a displayed suggestion.
The state could still become stale between showing and interacting with
a suggestion, but this CL only tackles the interval between interacting
with and actually executing suggestion acceptance.
Fixed: 501628355
Bug: 526550688
Change-Id: Ib18e05b435c84d46f52fd774304cf94d3ff2b35a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7967609
Reviewed-by: Jan Keitel <jkeitel@google.com>
Auto-Submit: Jihad Hanna <jihadghanna@google.com>
Commit-Queue: Jihad Hanna <jihadghanna@google.com>
Cr-Commit-Position: refs/heads/main@{#1650851}
---
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.cc b/components/autofill/core/browser/ui/autofill_external_delegate.cc
index 5a9543f..5b03e0e 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.cc
@@ -94,6 +94,54 @@
namespace {
+// Fills the queried form with the provided credit card using the specified
+// trigger source. Used as a callback for asynchronous card fetches.
+void OnCreditCardFetched(base::WeakPtr<BrowserAutofillManager> manager,
+ AutofillTriggerSource trigger_source,
+ const FormGlobalId& form_id,
+ const FieldGlobalId& field_id,
+ const CreditCard& card) {
+ if (manager) {
+ manager->FillOrPreviewForm(mojom::ActionPersistence::kFill, form_id,
+ field_id, &card, trigger_source,
+ /*blocked_fields=*/{});
+ }
+}
+
+// Fills the queried form with the provided `EntityInstance` in `result`,
+// unless a `FailureReason` is present.
+void OnEntityInstanceFetched(
+ base::WeakPtr<BrowserAutofillManager> manager,
+ AutofillTriggerSource trigger_source,
+ const FormGlobalId& form_id,
+ const FieldGlobalId& field_id,
+ const FieldTypeSet& ai_field_types,
+ base::expected<EntityInstance, AutofillAiAccessManager::FailureReason>
+ result,
+ bool reauth_attempted) {
+ if (!manager) {
+ return;
+ }
+ if (reauth_attempted) {
+ const bool auth_succeeded =
+ result.has_value() ||
+ result.error() != AutofillAiAccessManager::FailureReason::kReauthFailed;
+ LogReauthToFillResultPerFieldType(ai_field_types, auth_succeeded);
+ }
+
+ if (result.has_value()) {
+ manager->FillOrPreviewForm(mojom::ActionPersistence::kFill, form_id,
+ field_id, &result.value(), trigger_source,
+ /*blocked_fields=*/{});
+ } else if (result.error() ==
+ AutofillAiAccessManager::FailureReason::kFetchFailed) {
+ manager->client().ShowAutofillAiFetchFromWalletFailureNotification();
+ }
+
+ manager->client().HideSuggestions(SuggestionHidingReason::kAcceptSuggestion,
+ FillingProduct::kAutofillAi);
+}
+
std::optional<AutofillProfile> GetTestAddressByGUID(
base::span<const AutofillProfile> test_addresses,
const std::string& guid) {
@@ -841,8 +889,10 @@
const bool is_async =
manager_->GetAutofillAiAccessManager().FetchEntityInstance(
*entity, will_fill_sensitive_info,
- base::BindOnce(&AutofillExternalDelegate::OnEntityInstanceFetched,
- GetWeakPtr(), GetTriggerSource(),
+ base::BindOnce(&OnEntityInstanceFetched,
+ manager_->GetBrowserAutofillManagerWeakPtr(),
+ GetTriggerSource(), last_query_.form_id,
+ last_query_.field_id,
autofill_field->Type().GetAutofillAiTypes()));
if (is_async &&
@@ -879,9 +929,11 @@
identity_credential_delegate->NotifySuggestionAccepted(
suggestion, /*show_modal=*/true,
base::BindOnce(
- [](base::WeakPtr<AutofillExternalDelegate> delegate,
- const Suggestion& suggestion, bool accepted) {
- if (!delegate || !accepted) {
+ [](base::WeakPtr<BrowserAutofillManager> manager,
+ const Suggestion& suggestion, const FormGlobalId& form_id,
+ const FieldGlobalId& field_id,
+ AutofillTriggerSource trigger_source, bool accepted) {
+ if (!manager || !accepted) {
return;
}
@@ -889,15 +941,14 @@
suggestion
.GetPayload<Suggestion::IdentityCredentialPayload>()
.fields;
- delegate->manager_->FillOrPreviewForm(
- mojom::ActionPersistence::kFill,
- delegate->last_query_.form_id,
- delegate->last_query_.field_id, &profile,
- TriggerSourceFromSuggestionTriggerSource(
- delegate->trigger_source_),
- /*blocked_fields=*/{});
+ manager->FillOrPreviewForm(mojom::ActionPersistence::kFill,
+ form_id, field_id, &profile,
+ trigger_source,
+ /*blocked_fields=*/{});
},
- GetWeakPtr(), suggestion));
+ manager_->GetBrowserAutofillManagerWeakPtr(), suggestion,
+ last_query_.form_id, last_query_.field_id,
+ TriggerSourceFromSuggestionTriggerSource(trigger_source_)));
}
break;
}
@@ -1124,19 +1175,20 @@
switch (tab_type) {
case TabbedPaneTabType::kPayLater:
manager_->GetPaymentsBnplManager()->OnUserDecisionToUseBnpl(
- std::nullopt, base::BindOnce(
- [](base::WeakPtr<AutofillExternalDelegate> delegate,
- const CreditCard& card) {
- if (delegate) {
- delegate->manager_->FillOrPreviewForm(
- mojom::ActionPersistence::kFill,
- delegate->last_query_.form_id,
- delegate->last_query_.field_id, &card,
- AutofillTriggerSource::kPopup,
- /*blocked_fields=*/{});
- }
- },
- GetWeakPtr()));
+ std::nullopt,
+ base::BindOnce(
+ [](base::WeakPtr<BrowserAutofillManager> manager,
+ const FormGlobalId& form_id, const FieldGlobalId& field_id,
+ const CreditCard& card) {
+ if (manager) {
+ manager->FillOrPreviewForm(mojom::ActionPersistence::kFill,
+ form_id, field_id, &card,
+ AutofillTriggerSource::kPopup,
+ /*blocked_fields=*/{});
+ }
+ },
+ manager_->GetBrowserAutofillManagerWeakPtr(), last_query_.form_id,
+ last_query_.field_id));
break;
case TabbedPaneTabType::kPayNow:
manager_->GetPaymentsBnplManager()->OnUserDecisionToUseSavedCards();
@@ -1165,42 +1217,6 @@
return weak_ptr_factory_.GetWeakPtr();
}
-void AutofillExternalDelegate::OnCreditCardFetched(
- AutofillTriggerSource trigger_source,
- const CreditCard& card) {
- manager_->FillOrPreviewForm(mojom::ActionPersistence::kFill,
- last_query_.form_id, last_query_.field_id, &card,
- trigger_source,
- /*blocked_fields=*/{});
-}
-
-void AutofillExternalDelegate::OnEntityInstanceFetched(
- AutofillTriggerSource trigger_source,
- const FieldTypeSet& ai_field_types,
- base::expected<EntityInstance, AutofillAiAccessManager::FailureReason>
- result,
- bool reauth_attempted) {
- if (reauth_attempted) {
- const bool auth_succeeded =
- result.has_value() ||
- result.error() != AutofillAiAccessManager::FailureReason::kReauthFailed;
- LogReauthToFillResultPerFieldType(ai_field_types, auth_succeeded);
- }
-
- if (result.has_value()) {
- manager_->FillOrPreviewForm(mojom::ActionPersistence::kFill,
- last_query_.form_id, last_query_.field_id,
- &result.value(), trigger_source,
- /*blocked_fields=*/{});
- } else if (result.error() ==
- AutofillAiAccessManager::FailureReason::kFetchFailed) {
- manager_->client().ShowAutofillAiFetchFromWalletFailureNotification();
- }
-
- manager_->client().HideSuggestions(SuggestionHidingReason::kAcceptSuggestion,
- FillingProduct::kAutofillAi);
-}
-
void AutofillExternalDelegate::PreviewAddressFieldByFieldFillingSuggestion(
const AutofillProfile& profile,
const Suggestion& suggestion) {
@@ -1434,20 +1450,21 @@
manager_->client()
.GetPaymentsAutofillClient()
->GetIbanAccessManager()
- ->FetchValue(suggestion.payload,
- base::BindOnce(
- [](base::WeakPtr<AutofillExternalDelegate> delegate,
- const std::u16string& value) {
- if (delegate) {
- delegate->manager_->FillOrPreviewField(
- mojom::ActionPersistence::kFill,
- mojom::FieldActionType::kReplaceAll,
- delegate->last_query_.form_id,
- delegate->last_query_.field_id, value,
- FillingProduct::kIban, IBAN_VALUE);
- }
- },
- GetWeakPtr()));
+ ->FetchValue(
+ suggestion.payload,
+ base::BindOnce(
+ [](base::WeakPtr<BrowserAutofillManager> manager,
+ const FormGlobalId& form_id, const FieldGlobalId& field_id,
+ const std::u16string& value) {
+ if (manager) {
+ manager->FillOrPreviewField(
+ mojom::ActionPersistence::kFill,
+ mojom::FieldActionType::kReplaceAll, form_id,
+ field_id, value, FillingProduct::kIban, IBAN_VALUE);
+ }
+ },
+ manager_->GetBrowserAutofillManagerWeakPtr(),
+ last_query_.form_id, last_query_.field_id));
manager_->OnSingleFieldSuggestionSelected(suggestion, last_query_.form_id,
last_query_.field_id);
break;
@@ -1477,9 +1494,10 @@
CHECK(save_and_fill_manager);
save_and_fill_manager->OnDidAcceptCreditCardSaveAndFillSuggestion(
- base::BindOnce(&AutofillExternalDelegate::OnCreditCardFetched,
- GetWeakPtr(),
- AutofillTriggerSource::kCreditCardSaveAndFill));
+ base::BindOnce(&OnCreditCardFetched,
+ manager_->GetBrowserAutofillManagerWeakPtr(),
+ AutofillTriggerSource::kCreditCardSaveAndFill,
+ last_query_.form_id, last_query_.field_id));
manager_->GetCreditCardFormEventLogger()
.OnDidAcceptSaveAndFillSuggestion();
@@ -1487,8 +1505,10 @@
}
case SuggestionType::kScanCreditCard:
manager_->client().GetPaymentsAutofillClient()->ScanCreditCard(
- base::BindOnce(&AutofillExternalDelegate::OnCreditCardFetched,
- GetWeakPtr(), AutofillTriggerSource::kScanCreditCard));
+ base::BindOnce(&OnCreditCardFetched,
+ manager_->GetBrowserAutofillManagerWeakPtr(),
+ AutofillTriggerSource::kScanCreditCard,
+ last_query_.form_id, last_query_.field_id));
break;
case SuggestionType::kBnplEntry: {
payments::BnplManager* bnpl_manager = manager_->GetPaymentsBnplManager();
@@ -1503,8 +1523,10 @@
/*final_checkout_amount=*/suggestion
.GetPayload<Suggestion::PaymentsPayload>()
.extracted_amount_in_micros,
- base::BindOnce(&AutofillExternalDelegate::OnCreditCardFetched,
- GetWeakPtr(), AutofillTriggerSource::kPopup));
+ base::BindOnce(&OnCreditCardFetched,
+ manager_->GetBrowserAutofillManagerWeakPtr(),
+ AutofillTriggerSource::kPopup, last_query_.form_id,
+ last_query_.field_id));
}
break;
}
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.h b/components/autofill/core/browser/ui/autofill_external_delegate.h
index b5f9602e..672873aa 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.h
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.h
@@ -47,7 +47,6 @@
class AddressDataManager;
class AutofillDriver;
class BrowserAutofillManager;
-class CreditCard;
class FormStructure;
// Retrieves a copy of the profile that the `payload` refers to.
@@ -186,20 +185,6 @@
void DidAcceptPaymentsSuggestion(const Suggestion& suggestion,
const SuggestionMetadata& metadata);
- // Fills the queried form with the provided credit card using the specified
- // trigger source. Used as a callback for asynchronous card fetches.
- void OnCreditCardFetched(AutofillTriggerSource trigger_source,
- const CreditCard& card);
-
- // Fills the queried form with the provided `EntityInstance` in `result`,
- // unless a `FailureReason` is present.
- void OnEntityInstanceFetched(
- AutofillTriggerSource trigger_source,
- const FieldTypeSet& ai_field_types,
- base::expected<EntityInstance, AutofillAiAccessManager::FailureReason>
- result,
- bool reauth_attempted);
-
// Returns the last Autofill triggering field. Derived from the `form` and
// `field` parameters of `OnQuery(). Returns nullptr if called before
// `OnQuery()` or if the `form` becomes outdated, see crbug.com/1117028.