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 <[email protected]> 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 <[email protected]> Auto-Submit: Jihad Hanna <[email protected]> Commit-Queue: Jihad Hanna <[email protected]> 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.