Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Autofill
DescriptionInappropriate implementation in Autofill
ComponentAutofill
Bug ClassLogic Error
Tracker501628355
Fix commit1d6c6b5c9f9b (chromium/src) +105/-98
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

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

Files Changed

  • components/autofill/core/browser/ui/autofill_external_delegate.cc
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();
Loading diff…

Original Bug Report

reported by [email protected]

Potential cross-origin payment data leak via TOCTOU in AutofillExternalDelegate

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 without the Chrome Security team.

Overview: A Time-Of-Check-To-Time-Of-Use (TOCTOU) logic flaw exists in AutofillExternalDelegate, where asynchronous payment callbacks rely on mutable class members. A malicious page can overwrite these members during a pending asynchronous operation, bypassing cross-origin security checks and exfiltrating sensitive payment data.

Affected files:

  • components/autofill/core/browser/ui/autofill_external_delegate.cc
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc
  • components/autofill/core/browser/foundations/autofill_driver_router.cc
  • components/autofill/core/browser/filling/form_filler.cc
  • components/autofill/core/browser/foundations/form_forest.cc
  • components/autofill/content/browser/content_autofill_driver.cc

Estimated timestamp from git blame: 2026-04-01

Summary

A potential Time-Of-Check-To-Time-Of-Use (TOCTOU) vulnerability has been identified in the Autofill system. AutofillExternalDelegate caches the target form and field metadata for the current Autofill query in mutable member variables (query_form_ and query_field_). When a user selects a suggestion that triggers an asynchronous operation (such as Scan Credit Card, Server IBAN Unmasking, or BNPL flows), a callback is bound to handle the completion of the operation.

However, the bound callbacks (e.g., OnCreditCardFetched) do not capture the state of the form at the time the operation was initiated. Instead, they read the current values of query_form_ and query_field_ when they execute. A malicious site can exploit this window by programmatically triggering a new Autofill query for an attacker-controlled field before the asynchronous operation completes. This overwrites the cached variables, directing the sensitive payment credentials into the attacker’s field and bypassing cross-origin security checks.

Potential Attack Scenario

The following steps describe how an attacker could potentially exploit this vulnerability. Please note that these are suggested steps based on source code analysis, and a working proof-of-concept has not been executed.

  1. Frame Setup: An attacker at https://attacker.com/ embeds a legitimate cross-origin iframe (e.g., https://psp.example/ containing a checkout form) inside their own <form> element. They also place a hidden <textarea> within their form.
  2. Form Stitching: Because the iframe is nested in the attacker’s form, FormForest links them. Requests from the victim iframe are routed to the attacker’s BrowserAutofillManager and its associated AutofillExternalDelegate.
  3. Initial Interaction: The user clicks a credit card field in the legitimate psp.example iframe. This triggers AutofillExternalDelegate::OnQuery, which saves the psp.example form and field into query_form_ and query_field_.
  4. Asynchronous Trigger: The user selects an asynchronous payment option (e.g., “Scan new card”). The delegate starts the native camera UI and binds the OnCreditCardFetched callback.
  5. The Race (TOCTOU): While the camera UI is pending, the attacker’s JavaScript programmatically calls focus() on its hidden <textarea>. This triggers a new AskForValuesToFill IPC (via the kTextareaFocusedWithoutClick trigger source).
  6. State Overwrite: The browser processes the IPC and unconditionally calls AutofillExternalDelegate::OnQuery again, overwriting query_form_ and query_field_ with the attacker’s textarea metadata.
  7. Security Bypass and Exfiltration: The user finishes scanning their card. OnCreditCardFetched executes and calls FillOrPreviewForm using the poisoned query_field_. The security check in FormForest::IsSafeToFill verifies if the target field’s origin matches the triggered_origin. Since both are now derived from the attacker’s textarea, the check passes. The full PAN and expiry date are routed to the attacker’s <textarea>.

Suggested Fix

Callbacks for asynchronous operations should not rely on mutable class members (query_form_ and query_field_) that can be altered by concurrent events.

The fix should involve capturing the current FormData and FieldGlobalId (or FormFieldData) directly into the callback bindings when the asynchronous operation is initiated. For example, OnCreditCardFetched should be modified to accept the target form and field ID as parameters, ensuring that the fetched data is always returned to the specific field the user originally interacted with.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.

View on issue tracker
Links in the report