CVE-2026-17753
Overview
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 |
Files Changed
components/autofill/core/browser/ui/autofill_external_delegate.cc
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();
Original Bug Report
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.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.cccomponents/autofill/core/browser/foundations/autofill_driver_router.cccomponents/autofill/core/browser/filling/form_filler.cccomponents/autofill/core/browser/foundations/form_forest.cccomponents/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.
- 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. - Form Stitching: Because the iframe is nested in the attacker’s form,
FormForestlinks them. Requests from the victim iframe are routed to the attacker’sBrowserAutofillManagerand its associatedAutofillExternalDelegate. - Initial Interaction: The user clicks a credit card field in the legitimate
psp.exampleiframe. This triggersAutofillExternalDelegate::OnQuery, which saves thepsp.exampleform and field intoquery_form_andquery_field_. - Asynchronous Trigger: The user selects an asynchronous payment option (e.g., “Scan new card”). The delegate starts the native camera UI and binds the
OnCreditCardFetchedcallback. - The Race (TOCTOU): While the camera UI is pending, the attacker’s JavaScript programmatically calls
focus()on its hidden<textarea>. This triggers a newAskForValuesToFillIPC (via thekTextareaFocusedWithoutClicktrigger source). - State Overwrite: The browser processes the IPC and unconditionally calls
AutofillExternalDelegate::OnQueryagain, overwritingquery_form_andquery_field_with the attacker’s textarea metadata. - Security Bypass and Exfiltration: The user finishes scanning their card.
OnCreditCardFetchedexecutes and callsFillOrPreviewFormusing the poisonedquery_field_. The security check inFormForest::IsSafeToFillverifies if the target field’s origin matches thetriggered_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.