Medium watch CVSS 5.3 chrome Race 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in Transactions Platform
DescriptionRace condition in Transactions Platform
ComponentTransactions Platform
Bug ClassRace
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker526550688
Fix commit1d6c6b5c9f9b (chromium/src) +105/-98
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-22 (per vendor advisory)
Fix landed2026-06-23 (public git)
Disclosed2026-09-22
Patch gap91 days — fix public in source before the advisory

Background

`AutofillExternalDelegate`
the browser-side object that mediates between a shown Autofill popup and the BrowserAutofillManager, holding per-interaction state such as last_query_ and trigger_source_.
`last_query_`
a member struct on the delegate recording the FormGlobalId and FieldGlobalId of 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/WeakPtr is 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.

Key insight
The core mistake was reading mutable per-interaction state (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

  1. 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::BindOnce to capture a WeakPtr<AutofillExternalDelegate> bound to read last_query_ later.
  2. 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, and trigger_source_ on the same delegate. (inferred)
  3. Callback fills the stale target The async callback completes and calls FillOrPreviewForm using the now-updated last_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)
  4. 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

An attacker gains a race-dependent chance to have autofilled data (credit card, AutofillAI entity, or identity-credential fields) written to a form field other than the one the user intended, within the browser process handling Autofill on the renderer-driven page. The precondition is winning the timing window between accepting an async-fetch suggestion and the fetch completing, during which the delegate’s query state is overwritten. The advisory classifies this as a race condition in the Transactions Platform component and does not state exploited-in-the-wild status; any escalation to reliable sensitive-data exfiltration is (inferred).

Changed Functions

FunctionChangeNotes
if
components/autofill/core/browser/ui/autofill_external_delegate.cc
modified
switch
components/autofill/core/browser/ui/autofill_external_delegate.cc
modified
AddressDataManager
components/autofill/core/browser/ui/autofill_external_delegate.h
modified
AutofillDriver
components/autofill/core/browser/ui/autofill_external_delegate.h
modified
BrowserAutofillManager
components/autofill/core/browser/ui/autofill_external_delegate.h
modified
CreditCard
components/autofill/core/browser/ui/autofill_external_delegate.h
modified
FormStructure
components/autofill/core/browser/ui/autofill_external_delegate.h
modified

Files Changed

  • components/autofill/core/browser/ui/autofill_external_delegate.cc
  • components/autofill/core/browser/ui/autofill_external_delegate.h

Audit Directions

  • Deferred state reads in async callbacks
    Audit other Autofill callbacks that capture a delegate/manager WeakPtr and read last_query_, trigger_source_, or similar per-interaction members at completion time rather than binding them as arguments.
  • Cross-query mutation windows
    Look 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 changes
    Flag patterns where a WeakPtr liveness check is treated as sufficient for correctness while the object’s fields may have been mutated between bind and invocation.
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.
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.