Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Transactions Platform
DescriptionUse after free in Transactions Platform
ComponentTransactions Platform
Sub-componentAutofill
Bug ClassUAF
Tracker553128833
Fix commitbf1633dbb56c (chromium/src) +373/-146
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
modified
if
chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
modified
GetAnchorFrameToken
chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
modified
if
chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
modified
switch
components/autofill/core/browser/ui/autofill_external_delegate.cc
modified

Files Changed

  • chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.h
  • components/autofill/core/browser/foundations/autofill_client.cc
  • chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
  • chrome/browser/ui/autofill/popup_controller_common.h
  • chrome/browser/ui/autofill/chrome_autofill_client.cc
  • components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
  • chrome/browser/ui/autofill/autofill_popup_controller_impl_mac_unittest.mm
  • components/autofill/ios/browser/autofill_agent_unittest.mm
  • components/autofill/core/browser/ui/autofill_external_delegate.h
  • components/autofill/core/browser/ui/payments/omnibox_autofill_delegate_unittest.cc
  • chrome/browser/ui/autofill/autofill_suggestion_controller.cc
  • components/autofill/core/browser/ui/mock_autofill_suggestion_delegate.h
  • chrome/browser/ui/autofill/popup_controller_common.cc
  • components/autofill/core/browser/ui/test_autofill_external_delegate.cc
  • components/password_manager/core/browser/password_autofill_manager.h
  • chrome/browser/ui/android/autofill/at_memory_bottom_sheet_bridge_unittest.cc
  • components/password_manager/core/browser/password_manual_fallback_flow.h
  • chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.cc
  • components/autofill/core/browser/ui/autofill_external_delegate.cc
  • components/autofill/core/browser/foundations/autofill_client.h
  • chrome/browser/ui/passwords/password_generation_popup_controller_impl.cc
  • chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
  • chrome/browser/ui/autofill/at_memory_suggestion_controller_unittest.cc
  • components/password_manager/core/browser/password_autofill_manager.cc
  • components/autofill/core/browser/ui/payments/omnibox_autofill_delegate.h
  • chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
  • components/autofill/core/browser/ui/payments/omnibox_autofill_delegate.cc
  • chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc
  • chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
  • components/autofill/core/browser/ui/autofill_suggestion_delegate.h
  • components/autofill/core/browser/ui/test_autofill_external_delegate.h
  • components/autofill/ios/browser/autofill_agent.mm
  • components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
  • chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
  • chrome/browser/ui/autofill/chrome_autofill_client_interactive_uitest.cc
  • components/password_manager/core/browser/password_manual_fallback_flow.cc
From bf1633dbb56cf28aee80ccd4d9eba3549c409415 Mon Sep 17 00:00:00 2001
From: Christoph Schwering <schwering@google.com>
Date: Sat, 19 Sep 2026 15:48:40 -0700
Subject: [PATCH] [Autofill] Pass FormGlobalId and FieldGlobalId to suggestion delegates

This CL replaces
  Popup{ControllerCommon,OpenArgs}::anchor_frame_token
with
  Popup{ControllerCommon,OpenArgs}::form_id
  Popup{ControllerCommon,OpenArgs}::field_id
and passes them to
AutofillSuggestionDelegate::Did{Accept,Select}Suggestion().

The CL is a no-op: The role of `anchor_frame_token` is now filled by
`field_id.frame_token`. And the parameters are not used yet in
Did{Accept,Select}Suggestion().

This is in preparation for crrev.com/c/8422781.

TAG=agy
CONV=87860559-e016-44b4-81dd-8635b7409a43

Bug: 563089510, 553128833
Change-Id: If311adbb6f47ccd685eec72ebcb73c5cc04af0e1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8419078
Reviewed-by: Mohamed Amir Yosef <mamir@chromium.org>
Commit-Queue: Christoph Schwering <schwering@google.com>
Cr-Commit-Position: refs/heads/main@{#1701425}
---
diff --git a/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc b/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
index 01606b6..859fef4 100644
--- a/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
+++ b/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
@@ -92,8 +92,10 @@
   Suggestion suggestion = suggestions_[index];
   if (delegate_) {
     delegate_->DidAcceptSuggestion(
-        suggestion, AutofillSuggestionDelegate::SuggestionMetadata{
-                        .multi_index = {static_cast<size_t>(index)}});
+        suggestion,
+        AutofillSuggestionDelegate::SuggestionMetadata{
+            .multi_index = {static_cast<size_t>(index)}},
+        controller_common_.form_id, controller_common_.field_id);
   }
 }
 
@@ -171,7 +173,7 @@
 
 const LocalFrameToken& AtMemorySuggestionController::GetAnchorFrameToken()
     const {
-  return controller_common_.anchor_frame_token;
+  return controller_common_.field_id.frame_token;
 }
 
 void AtMemorySuggestionController::HideViewAndDie() {
@@ -290,9 +292,11 @@
   Suggestion suggestion = parent_suggestion.children[child_position];
   if (delegate_) {
     delegate_->DidAcceptSuggestion(
-        suggestion, AutofillSuggestionDelegate::SuggestionMetadata{
-                        .multi_index = {static_cast<size_t>(parent_position),
-                                        static_cast<size_t>(child_position)}});
+        suggestion,
+        AutofillSuggestionDelegate::SuggestionMetadata{
+            .multi_index = {static_cast<size_t>(parent_position),
+                            static_cast<size_t>(child_position)}},
+        controller_common_.form_id, controller_common_.field_id);
   }
 }
 
diff --git a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
index e4b856a..ff83f370 100644
--- a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
+++ b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
@@ -593,8 +593,10 @@
     mouse_metrics_recorder_->RecordAccepted(suggestions_filling_product_);
   }
   delegate_->DidAcceptSuggestion(
-      suggestion, AutofillSuggestionDelegate::SuggestionMetadata{
-                      .multi_index = {static_cast<size_t>(index)}});
+      suggestion,
+      AutofillSuggestionDelegate::SuggestionMetadata{
+          .multi_index = {static_cast<size_t>(index)}},
+      controller_common_.form_id, controller_common_.field_id);
 }
 
 bool AutofillKeyboardAccessoryControllerImpl::RemoveSuggestion(int index) {
@@ -741,7 +743,7 @@
   }
 
   content::RenderFrameHost* rfh = FindRenderFrameHostByToken(
-      *web_contents_, controller_common_.anchor_frame_token);
+      *web_contents_, controller_common_.field_id.frame_token);
   if (!rfh) {
     Hide(SuggestionHidingReason::kNoFrameHasFocus);
     return;
@@ -855,7 +857,7 @@
 
 const LocalFrameToken&
 AutofillKeyboardAccessoryControllerImpl::GetAnchorFrameToken() const {
-  return controller_common_.anchor_frame_token;
+  return controller_common_.field_id.frame_token;
 }
 
 bool AutofillKeyboardAccessoryControllerImpl::HasSuggestions() const {
@@ -1029,7 +1031,8 @@
     if (mouse_metrics_recorder_) {
       mouse_metrics_recorder_->RecordSelected(suggestions_filling_product_);
     }
-    delegate_->DidSelectSuggestion(suggestion);
+    delegate_->DidSelectSuggestion(suggestion, controller_common_.form_id,
+                                   controller_common_.field_id);
   } else {
     delegate_->ClearPreviewedForm();
   }
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc b/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
index 9be4b1b..60a24350 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
@@ -331,7 +331,7 @@
   }
 
   content::RenderFrameHost* rfh = FindRenderFrameHostByToken(
-      *web_contents_, controller_common_.anchor_frame_token);
+      *web_contents_, controller_common_.field_id.frame_token);
   if (!rfh) {
     Hide(SuggestionHidingReason::kNoFrameHasFocus);
     return;
@@ -468,7 +468,7 @@
 
 const LocalFrameToken& AutofillPopupControllerImpl::GetAnchorFrameToken()
     const {
-  return controller_common_.anchor_frame_token;
+  return controller_common_.field_id.frame_token;
 }
 
 bool AutofillPopupControllerImpl::IsViewVisibilityAcceptingThresholdEnabled()
@@ -571,7 +571,9 @@
   base::UmaHistogramEnumeration("Autofill.SuggestionAccepted.Method",
                                 accept_method);
 
-  delegate_->DidAcceptSuggestion(suggestion, GetSuggestionMetadata(index));
+  delegate_->DidAcceptSuggestion(suggestion, GetSuggestionMetadata(index),
+                                 controller_common_.form_id,
+                                 controller_common_.field_id);
 }
 
 AutofillSuggestionDelegate::SuggestionUiMetadata
@@ -930,7 +932,9 @@
   }
 
   any_suggestion_selected_ = true;
-  delegate_->DidSelectSuggestion(GetSuggestionAt(index));
+  delegate_->DidSelectSuggestion(GetSuggestionAt(index),
+                                 controller_common_.form_id,
+                                 controller_common_.field_id);
 }
 
 void AutofillPopupControllerImpl::UnselectSuggestion() {
diff --git a/chrome/browser/ui/autofill/autofill_suggestion_controller.cc b/chrome/browser/ui/autofill/autofill_suggestion_controller.cc
index 5ddaf13..641314c 100644
--- a/chrome/browser/ui/autofill/autofill_suggestion_controller.cc
+++ b/chrome/browser/ui/autofill/autofill_suggestion_controller.cc
@@ -78,7 +78,7 @@
     int32_t form_control_ax_id,
     AutofillSuggestionTriggerSource trigger_source) {
   if (previous && previous->MayRecycle(delegate, web_contents,
-                                       controller_common.anchor_frame_token,
+                                       controller_common.field_id.frame_token,
                                        trigger_source)) {
     previous->Recycle(std::move(controller_common), form_control_ax_id);
     return previous;
diff --git a/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.cc b/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.cc
index dd51e99..1a1e295 100644
--- a/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.cc
+++ b/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.cc
@@ -50,7 +50,8 @@
     : AutofillSuggestionControllerForTestBase(
           external_delegate,
           web_contents,
-          PopupControllerCommon(frame_token,
+          PopupControllerCommon(FormGlobalId(frame_token, FormRendererId()),
+                                FieldGlobalId(frame_token, FieldRendererId()),
                                 element_bounds,
                                 base::i18n::UNKNOWN_DIRECTION)) {}
 
diff --git a/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.h b/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.h
index 882913f..d037578 100644
--- a/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.h
+++ b/chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.h
@@ -272,11 +272,16 @@
                const AutofillSuggestionDelegate::SuggestionUiMetadata&),
               (override));
   MOCK_METHOD(void, OnSuggestionsHidden, (SuggestionHidingReason), (override));
-  MOCK_METHOD(void, DidSelectSuggestion, (const Suggestion&), (override));
+  MOCK_METHOD(void,
+              DidSelectSuggestion,
+              (const Suggestion&, const FormGlobalId&, const FieldGlobalId&),
+              (override));
   MOCK_METHOD(void,
               DidAcceptSuggestion,
               (const Suggestion&,
-               const AutofillSuggestionDelegate::SuggestionMetadata&),
+               const AutofillSuggestionDelegate::SuggestionMetadata&,
+               const FormGlobalId&,
+               const FieldGlobalId&),
               (override));
   MOCK_METHOD(bool, RemoveSuggestion, (const Suggestion&), (override));
   MOCK_METHOD(void, OnTabSelected, (TabbedPaneTabType), (override));
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.cc b/chrome/browser/ui/autofill/chrome_autofill_client.cc
index 7ac6d9aa..b12118cc 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.cc
@@ -1445,7 +1445,7 @@
   suggestion_controller_ = AutofillSuggestionController::GetOrCreate(
       suggestion_controller_, delegate, web_contents(),
       PopupControllerCommon(
-          open_args.anchor_frame_token, element_bounds_in_screen_space,
+          open_args.form_id, open_args.field_id, element_bounds_in_screen_space,
           open_args.text_direction, open_args.anchor_type,
           open_args.show_tabbed_popup,
           open_args.prefer_prev_arrow_side_on_suggestions_update),
diff --git a/chrome/browser/ui/autofill/popup_controller_common.cc b/chrome/browser/ui/autofill/popup_controller_common.cc
index fd4393d..1497f1a5 100644
--- a/chrome/browser/ui/autofill/popup_controller_common.cc
+++ b/chrome/browser/ui/autofill/popup_controller_common.cc
@@ -11,13 +11,15 @@
 namespace autofill {
 
 PopupControllerCommon::PopupControllerCommon(
-    LocalFrameToken anchor_frame_token,
+    FormGlobalId form_id,
+    FieldGlobalId field_id,
     gfx::RectF element_bounds,
     base::i18n::TextDirection text_direction,
     PopupAnchorType anchor_type,
     bool show_tabbed_popup,
     bool prefer_prev_arrow_side_on_suggestions_update)
-    : anchor_frame_token(std::move(anchor_frame_token)),
+    : form_id(std::move(form_id)),
+      field_id(std::move(field_id)),
       element_bounds(std::move(element_bounds)),
       text_direction(text_direction),
       anchor_type(anchor_type),
diff --git a/chrome/browser/ui/autofill/popup_controller_common.h b/chrome/browser/ui/autofill/popup_controller_common.h
index 85050ea8..3fc2a636 100644
--- a/chrome/browser/ui/autofill/popup_controller_common.h
+++ b/chrome/browser/ui/autofill/popup_controller_common.h
@@ -18,7 +18,8 @@
 struct PopupControllerCommon {
  public:
   PopupControllerCommon(
-      LocalFrameToken anchor_frame_token,
+      FormGlobalId form_id,
+      FieldGlobalId field_id,
       gfx::RectF element_bounds,
       base::i18n::TextDirection text_direction,
       PopupAnchorType anchor_type = PopupAnchorType::kField,
@@ -31,9 +32,9 @@
 
   ~PopupControllerCommon();
 
-  // The frame in which the popup is anchored. Typically this is the frame of
-  // the field on which the user triggered Autofill.
-  LocalFrameToken anchor_frame_token;
+  // The form and field in which the popup is anchored.
+  FormGlobalId form_id;
+  FieldGlobalId field_id;
 
   // The bounds of the DOM element that is the focus of the popup.
   // These coordinates are in screen space.
diff --git a/chrome/browser/ui/passwords/password_generation_popup_controller_impl.cc b/chrome/browser/ui/passwords/password_generation_popup_controller_impl.cc
index cd519b8e..3e4463c 100644
--- a/chrome/browser/ui/passwords/password_generation_popup_controller_impl.cc
+++ b/chrome/browser/ui/passwords/password_generation_popup_controller_impl.cc
@@ -139,7 +139,10 @@
           autofill::FormControlType::kInputPassword)),
       generation_element_id_(ui_data.generation_element_id),
       max_length_(ui_data.max_length),
-      controller_common_(autofill::LocalFrameToken(*frame->GetFrameToken()),
+      controller_common_(ui_data.form_data.global_id(),
+                         autofill::FieldGlobalId(
+                             autofill::LocalFrameToken(*frame->GetFrameToken()),
+                             ui_data.generation_element_id),
                          bounds,
                          ui_data.text_direction),
       state_(kOfferGeneration),
diff --git a/components/autofill/core/browser/foundations/autofill_client.cc b/components/autofill/core/browser/foundations/autofill_client.cc
index 447d2aa..aaa3881 100644
--- a/components/autofill/core/browser/foundations/autofill_client.cc
+++ b/components/autofill/core/browser/foundations/autofill_client.cc
@@ -46,7 +46,8 @@
 
 AutofillClient::PopupOpenArgs::PopupOpenArgs() = default;
 AutofillClient::PopupOpenArgs::PopupOpenArgs(
-    LocalFrameToken anchor_frame_token,
+    FormGlobalId form_id,
+    FieldGlobalId field_id,
     const gfx::RectF& element_bounds,
     base::i18n::TextDirection text_direction,
     std::vector<Suggestion> suggestions,
@@ -56,7 +57,8 @@
     bool show_tabbed_popup,
     bool prefer_prev_arrow_side_on_suggestions_update,
     std::u16string search_bar_initial_value)
-    : anchor_frame_token(std::move(anchor_frame_token)),
+    : form_id(std::move(form_id)),
+      field_id(std::move(field_id)),
       element_bounds(element_bounds),
       text_direction(text_direction),
       suggestions(std::move(suggestions)),
diff --git a/components/autofill/core/browser/foundations/autofill_client.h b/components/autofill/core/browser/foundations/autofill_client.h
index 9874a25..ad42d57 100644
--- a/components/autofill/core/browser/foundations/autofill_client.h
+++ b/components/autofill/core/browser/foundations/autofill_client.h
@@ -289,7 +289,8 @@
   // Required arguments to create a dropdown showing autofill suggestions.
   struct PopupOpenArgs {
     PopupOpenArgs();
-    PopupOpenArgs(LocalFrameToken anchor_frame_token,
+    PopupOpenArgs(FormGlobalId form_id,
+                  FieldGlobalId field_id,
                   const gfx::RectF& element_bounds,
                   base::i18n::TextDirection text_direction,
                   std::vector<Suggestion> suggestions,
@@ -304,9 +305,9 @@
     PopupOpenArgs& operator=(const PopupOpenArgs&);
     PopupOpenArgs& operator=(PopupOpenArgs&&);
     ~PopupOpenArgs();
-    // The frame which the popup is anchored to. Typically this is the frame of
-    // the field on which the user triggered Autofill.
-    LocalFrameToken anchor_frame_token;
+    // The form and field to which the popup is anchored.
+    FormGlobalId form_id;
+    FieldGlobalId field_id;
     // TODO(crbug.com/340817507): Update this member name since bounds can now
     // refer to the caret bounds and elements gives the idea of HTML elements
     // only.
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.cc b/components/autofill/core/browser/ui/autofill_external_delegate.cc
index 7291464..316d62e8 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.cc
@@ -607,7 +607,7 @@
 #endif
 
   AutofillClient::PopupOpenArgs open_args(
-      trigger_field->global_id().frame_token,
+      last_query_.form_id, trigger_field->global_id(),
       should_use_caret_bounds ? gfx::RectF(caret_bounds_)
                               : trigger_field->bounds(),
       trigger_field->text_direction(), std::move(suggestions), trigger_source_,
@@ -744,7 +744,9 @@
 }
 
 void AutofillExternalDelegate::DidSelectSuggestion(
-    const Suggestion& suggestion) {
+    const Suggestion& suggestion,
+    const FormGlobalId& form_id,
+    const FieldGlobalId& field_id) {
   ClearPreviewedForm();
 
   switch (suggestion.type) {
@@ -906,7 +908,9 @@
 
 void AutofillExternalDelegate::DidAcceptSuggestion(
     const Suggestion& suggestion,
-    const SuggestionMetadata& metadata) {
+    const SuggestionMetadata& metadata,
+    const FormGlobalId& form_id,
+    const FieldGlobalId& field_id) {
   CHECK(suggestion.IsAcceptable());
   // TODO(crbug.com/552871965): Extract the logging here to a separate function.
   base::UmaHistogramEnumeration("Autofill.Suggestions.AcceptedType",
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.h b/components/autofill/core/browser/ui/autofill_external_delegate.h
index 2ea75fb..0f8662a 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.h
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.h
@@ -79,9 +79,13 @@
   bool OnFilterChanged(const std::u16string& filter) override;
   bool OnSearchSubmitted(const std::u16string& filter) override;
   bool IsSearching() const override;
-  void DidSelectSuggestion(const Suggestion& suggestion) override;
+  void DidSelectSuggestion(const Suggestion& suggestion,
+                           const FormGlobalId& form_id,
+                           const FieldGlobalId& field_id) override;
   void DidAcceptSuggestion(const Suggestion& suggestion,
-                           const SuggestionMetadata& metadata) override;
+                           const SuggestionMetadata& metadata,
+                           const FormGlobalId& form_id,
+                           const FieldGlobalId& field_id) override;
   bool RemoveSuggestion(const Suggestion& suggestion) override;
   void ClearPreviewedForm() override;
 
diff --git a/components/autofill/core/browser/ui/autofill_suggestion_delegate.h b/components/autofill/core/browser/ui/autofill_suggestion_delegate.h
index ee33d84e..6e4c396 100644
--- a/components/autofill/core/browser/ui/autofill_suggestion_delegate.h
+++ b/components/autofill/core/browser/ui/autofill_suggestion_delegate.h
@@ -103,11 +103,15 @@
 
   // Called when the autofill `suggestion` has been temporarily selected (e.g.,
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/android/autofill/at_memory_bottom_sheet_bridge_unittest.cc b/chrome/browser/ui/android/autofill/at_memory_bottom_sheet_bridge_unittest.cc
index f09f169..4e590763 100644
--- a/chrome/browser/ui/android/autofill/at_memory_bottom_sheet_bridge_unittest.cc
+++ b/chrome/browser/ui/android/autofill/at_memory_bottom_sheet_bridge_unittest.cc
@@ -9,6 +9,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/ui/autofill/at_memory_suggestion_controller.h"
 #include "chrome/test/base/testing_profile.h"
+#include "components/autofill/core/common/autofill_test_util.h"
 #include "content/public/test/browser_task_environment.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -23,7 +24,8 @@
       : AtMemorySuggestionController(
             nullptr,
             nullptr,
-            PopupControllerCommon({},
+            PopupControllerCommon(test::MakeFormGlobalId(),
+                                  test::MakeFieldGlobalId(),
                                   gfx::RectF(),
                                   base::i18n::UNKNOWN_DIRECTION)) {}
   MOCK_METHOD(void, OnDismissed, (), (override));
@@ -39,6 +41,7 @@
   }
 
   content::BrowserTaskEnvironment task_environment_;
+  autofill::test::AutofillUnitTestEnvironment autofill_environment_;
   TestingProfile profile_;
   std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window_;
   std::unique_ptr<MockAtMemorySuggestionController> controller_;
diff --git a/chrome/browser/ui/autofill/at_memory_suggestion_controller_unittest.cc b/chrome/browser/ui/autofill/at_memory_suggestion_controller_unittest.cc
index 659c034..3095f10a 100644
--- a/chrome/browser/ui/autofill/at_memory_suggestion_controller_unittest.cc
+++ b/chrome/browser/ui/autofill/at_memory_suggestion_controller_unittest.cc
@@ -13,6 +13,7 @@
 #include "components/autofill/core/browser/suggestions/suggestion_hiding_reason.h"
 #include "components/autofill/core/browser/suggestions/suggestion_type.h"
 #include "components/autofill/core/browser/ui/mock_autofill_suggestion_delegate.h"
+#include "components/autofill/core/common/autofill_test_util.h"
 #include "content/public/test/navigation_simulator.h"
 #include "content/public/test/test_renderer_host.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -26,6 +27,16 @@
 using ::testing::ElementsAreArray;
 using ::testing::Eq;
 
+FormGlobalId MakeFormGlobalId(AutofillManager& manager) {
+  return FormGlobalId(manager.driver().GetFrameToken(),
+                      test::MakeFormRendererId());
+}
+
+FieldGlobalId MakeFieldGlobalId(AutofillManager& manager) {
+  return FieldGlobalId(manager.driver().GetFrameToken(),
+                       test::MakeFieldRendererId());
+}
+
 class MockAtMemoryBottomSheetBridge : public AtMemoryBottomSheetBridge {
  public:
   explicit MockAtMemoryBottomSheetBridge(
@@ -49,7 +60,8 @@
     if (!suggestion_controller_) {
       auto* controller = new AtMemorySuggestionController(
           manager.external_delegate().GetWeakPtrForTest(), &GetWebContents(),
-          PopupControllerCommon(manager.driver().GetFrameToken(), {},
+          PopupControllerCommon(MakeFormGlobalId(manager),
+                                MakeFieldGlobalId(manager), {},
                                 base::i18n::UNKNOWN_DIRECTION));
       suggestion_controller_ = controller->GetWeakPtr();
     }
@@ -167,7 +179,8 @@
       AutofillSuggestionController::GetOrCreate(
           /*previous=*/nullptr,
           manager1.external_delegate().GetWeakPtrForTest(), web_contents(),
-          PopupControllerCommon(manager1.driver().GetFrameToken(), {},
+          PopupControllerCommon(MakeFormGlobalId(manager1),
+                                MakeFieldGlobalId(manager1), {},
                                 base::i18n::UNKNOWN_DIRECTION),
           /*form_control_ax_id=*/0,
           AutofillSuggestionTriggerSource::kAtMemoryDoubleCtrl);
@@ -184,7 +197,8 @@
       AutofillSuggestionController::GetOrCreate(
           controller1_weak, manager2.external_delegate().GetWeakPtrForTest(),
           web_contents(),
-          PopupControllerCommon(manager2.driver().GetFrameToken(), {},
+          PopupControllerCommon(MakeFormGlobalId(manager2),
+                                MakeFieldGlobalId(manager2), {},
                                 base::i18n::UNKNOWN_DIRECTION),
           /*form_control_ax_id=*/0,
           AutofillSuggestionTriggerSource::kAtMemoryDoubleCtrl);
@@ -203,7 +217,8 @@
       AutofillSuggestionController::GetOrCreate(
           /*previous=*/nullptr,
           manager1.external_delegate().GetWeakPtrForTest(), web_contents(),
-          PopupControllerCommon(manager1.driver().GetFrameToken(), {},
+          PopupControllerCommon(MakeFormGlobalId(manager1),
+                                MakeFieldGlobalId(manager1), {},
                                 base::i18n::UNKNOWN_DIRECTION),
           /*form_control_ax_id=*/0,
           AutofillSuggestionTriggerSource::kAtMemoryDoubleCtrl);
@@ -212,7 +227,8 @@
       AutofillSuggestionController::GetOrCreate(
           controller1_weak, manager1.external_delegate().GetWeakPtrForTest(),
           web_contents(),
-          PopupControllerCommon(manager1.driver().GetFrameToken(), {},
+          PopupControllerCommon(MakeFormGlobalId(manager1),
+                                MakeFieldGlobalId(manager1), {},
                                 base::i18n::UNKNOWN_DIRECTION),
           /*form_control_ax_id=*/0,
           AutofillSuggestionTriggerSource::kAtMemoryDoubleCtrl);
@@ -236,7 +252,8 @@
           suggestions[0],
           testing::Field(
               &AutofillSuggestionDelegate::SuggestionMetadata::multi_index,
-              std::vector<size_t>{0})));
+              std::vector<size_t>{0}),
+          _, _));
 
   client().suggestion_controller(manager()).AcceptSuggestion(
       /*index=*/0, AutofillMetrics::SuggestionAcceptedMethod::kTap);
@@ -248,7 +265,9 @@
   testing::NiceMock<MockAutofillSuggestionDelegate> mock_delegate;
   auto* controller = new AtMemorySuggestionController(
       mock_delegate.GetWeakPtr(), web_contents(),
-      PopupControllerCommon({}, gfx::RectF(), base::i18n::UNKNOWN_DIRECTION));
+      PopupControllerCommon(MakeFormGlobalId(manager()),
+                            MakeFieldGlobalId(manager()), gfx::RectF(),
+                            base::i18n::UNKNOWN_DIRECTION));
 
   auto mock_bridge =
       std::make_unique<MockAtMemoryBottomSheetBridge>(controller);
@@ -281,7 +300,8 @@
           parent,
           testing::Field(
               &AutofillSuggestionDelegate::SuggestionMetadata::multi_index,
-              std::vector<size_t>{0})));
+              std::vector<size_t>{0}),
+          _, _));
   controller->OnSuggestionAccepted(0);
 
   // OnChildSuggestionsShown routes to OnSuggestionsShown with parent metadata.
@@ -296,7 +316,8 @@
           child,
           testing::Field(
               &AutofillSuggestionDelegate::SuggestionMetadata::multi_index,
-              std::vector<size_t>{0, 0})));
+              std::vector<size_t>{0, 0}),
+          _, _));
   controller->OnChildSuggestionSelected(0, 0);
 
   // IsSearching routes to IsSearching on delegate.
diff --git a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
index 6905193..bd6a32e 100644
--- a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
@@ -1186,8 +1186,11 @@
                         1)));
 
   client().suggestion_controller(manager()).Recycle(
-      PopupControllerCommon(manager().driver().GetFrameToken(), {},
-                            base::i18n::UNKNOWN_DIRECTION),
+      PopupControllerCommon(FormGlobalId(manager().driver().GetFrameToken(),
+                                         test::MakeFormRendererId()),
+                            FieldGlobalId(manager().driver().GetFrameToken(),
+                                          test::MakeFieldRendererId()),
+                            {}, base::i18n::UNKNOWN_DIRECTION),
       /*form_control_ax_id=*/0);
 
   ShowSuggestions(manager(), {test::CreateAutofillSuggestion(
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_impl_mac_unittest.mm b/chrome/browser/ui/autofill/autofill_popup_controller_impl_mac_unittest.mm
index 4221a0f..64ff954 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_impl_mac_unittest.mm
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_impl_mac_unittest.mm
@@ -11,6 +11,7 @@
 #import "chrome/browser/ui/cocoa/touchbar/web_textfield_touch_bar_controller.h"
 #import "components/autofill/core/browser/suggestions/suggestion.h"
 #import "components/autofill/core/browser/suggestions/suggestion_type.h"
+#import "components/autofill/core/common/autofill_test_util.h"
 #import "testing/gmock/include/gmock/gmock.h"
 #import "testing/gtest/include/gtest/gtest.h"
 #import "ui/gfx/geometry/rect_f.h"
@@ -41,9 +42,11 @@
       : AutofillPopupControllerImplMac(
             external_delegate,
             web_contents,
-            PopupControllerCommon(frame_token,
-                                  element_bounds,
-                                  base::i18n::UNKNOWN_DIRECTION)) {}
+            PopupControllerCommon(
+                FormGlobalId(frame_token, test::MakeFormRendererId()),
+                FieldGlobalId(frame_token, test::MakeFieldRendererId()),
+                element_bounds,
+                base::i18n::UNKNOWN_DIRECTION)) {}
 
   ~AutofillPopupControllerImplMacForTesting() override = default;
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc b/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc
index 5ec88031..d2208e5 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc
@@ -522,7 +522,7 @@
 
   EXPECT_CALL(manager().external_delegate(),
               DidAcceptSuggestion(
-                  _, EqualsSuggestionMetadata({.multi_index = {0, 0}})));
+                  _, EqualsSuggestionMetadata({.multi_index = {0, 0}}), _, _));
 
   task_environment()->FastForwardBy(base::Milliseconds(1000));
   sub_controller->AcceptSuggestion(
@@ -1179,10 +1179,12 @@
   ShowSuggestions(manager(),
                   {Suggestion(u"main_text", SuggestionType::kAddressEntry)});
 
-  EXPECT_CALL(manager().external_delegate(),
-              DidAcceptSuggestion(
-                  _, EqualsSuggestionMetadata(
-                         {.multi_index = {0}, .from_search_result = true})));
+  EXPECT_CALL(
+      manager().external_delegate(),
+      DidAcceptSuggestion(_,
+                          EqualsSuggestionMetadata(
+                              {.multi_index = {0}, .from_search_result = true}),
+                          _, _));
 
   controller.SetFilter(AutofillPopupController::StringFilter(u"main_text"),
                        AutofillPopupController::FilterSource::kInputChanged);
diff --git a/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc b/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
index 8412e26..24f2d9ab 100644
--- a/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
@@ -40,6 +40,7 @@
 #include "components/autofill/core/browser/ui/autofill_external_delegate.h"
 #include "components/autofill/core/browser/ui/autofill_suggestion_delegate.h"
 #include "components/autofill/core/common/aliases.h"
+#include "components/autofill/core/common/autofill_test_util.h"
 #include "components/autofill/core/common/unique_ids.h"
 #include "components/input/native_web_keyboard_event.h"
 #include "components/prefs/pref_service.h"
@@ -264,11 +265,14 @@
 
 TEST_F(AutofillSuggestionControllerTest, GetOrCreate) {
   auto create_controller = [&](gfx::RectF bounds) {
+    LocalFrameToken frame_token(*main_frame()->GetFrameToken());
     return AutofillSuggestionController::GetOrCreate(
         client().suggestion_controller(manager()).GetWeakPtr(),
         manager().external_delegate().GetWeakPtrForTest(), web_contents(),
-        PopupControllerCommon(LocalFrameToken(*main_frame()->GetFrameToken()),
-                              std::move(bounds), base::i18n::UNKNOWN_DIRECTION),
+        PopupControllerCommon(
+            FormGlobalId(frame_token, test::MakeFormRendererId()),
+            FieldGlobalId(frame_token, test::MakeFieldRendererId()),
+            std::move(bounds), base::i18n::UNKNOWN_DIRECTION),
         /*form_control_ax_id=*/0,
         AutofillSuggestionTriggerSource::kUnspecified);
   };
@@ -333,12 +337,15 @@
                               SuggestionType::kAutocompleteEntry});
 
   // Now show a new popup with the same controller, but with fewer items.
+  LocalFrameToken frame_token(*main_frame()->GetFrameToken());
   base::WeakPtr<AutofillSuggestionController> controller =
       AutofillSuggestionController::GetOrCreate(
           client().suggestion_controller(manager()).GetWeakPtr(),
           manager().external_delegate().GetWeakPtrForTest(), web_contents(),
-          PopupControllerCommon(LocalFrameToken(*main_frame()->GetFrameToken()),
-                                gfx::RectF(), base::i18n::UNKNOWN_DIRECTION),
+          PopupControllerCommon(
+              FormGlobalId(frame_token, test::MakeFormRendererId()),
+              FieldGlobalId(frame_token, test::MakeFieldRendererId()),
+              gfx::RectF(), base::i18n::UNKNOWN_DIRECTION),
           /*form_control_ax_id=*/0,
           AutofillSuggestionTriggerSource::kUnspecified);
   EXPECT_EQ(0, controller->GetLineCount());
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client_interactive_uitest.cc b/chrome/browser/ui/autofill/chrome_autofill_client_interactive_uitest.cc
index f2de98e..bbeb5f9 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client_interactive_uitest.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client_interactive_uitest.cc
@@ -25,6 +25,7 @@
 #include "components/autofill/core/browser/metrics/autofill_settings_metrics.h"
 #include "components/autofill/core/browser/suggestions/suggestion.h"
 #include "components/autofill/core/browser/suggestions/suggestion_type.h"
+#include "components/autofill/core/browser/test_utils/autofill_form_test_util.h"
 #include "components/autofill/core/browser/test_utils/autofill_test_util.h"
 #include "components/autofill/core/browser/ui/autofill_external_delegate.h"
 #include "components/autofill/core/browser/ui/popup_open_enums.h"
@@ -133,12 +134,12 @@
   }
 
   AutofillClient::SuggestionUiSessionId ShowSuggestions(
+      const FormGlobalId& form_id,
       const FieldGlobalId& field_id,
       const gfx::RectF& bounds) {
     return client()->ShowAutofillSuggestions(
         ChromeAutofillClient::PopupOpenArgs(
-            field_id.frame_token, bounds,
-            base::i18n::TextDirection::LEFT_TO_RIGHT,
+            form_id, field_id, bounds, base::i18n::TextDirection::LEFT_TO_RIGHT,
             {Suggestion(u"test", SuggestionType::kAutocompleteEntry)},
             AutofillSuggestionTriggerSource::kFormControlElementClicked,
             /*form_control_ax_id=*/0, PopupAnchorType::kField),
@@ -179,16 +180,19 @@
 // that the IPH is hidden and the Autofill Popup is successfully shown.
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-origin payment card leak via Autofill popup and last_query_ desync

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A state desynchronization between an active Autofill popup and AutofillExternalDelegate::last_query_ may allow a compromised renderer to divert accepted autofill data. When a user accepts saved payment card suggestions in a legitimate popup anchored to a cross-origin payment iframe, the browser fills through last_query_, which can be repointed to attacker-controlled fields without dismissing the popup. This could result in sensitive payment credentials being filled into attacker-origin fields instead of the target iframe.

Affected files:

  • components/autofill/core/browser/ui/autofill_external_delegate.cc
  • chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc
  • components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc

Estimated timestamp from git blame: 2026-08-13

Summary

There is a potential vulnerability in Chromium’s Autofill architecture where AutofillExternalDelegate::last_query_ can be desynchronized from an open Autofill suggestions popup. A compromised renderer embedding a cross-origin payment iframe can park last_query_ on its own fields while the user’s saved payment card popup remains open and interactive over the payment iframe. When the user accepts a card suggestion, the payment credentials (full PAN, expiry, cardholder name, and CVC if stored/unmasked) are filled into the attacker’s fields instead of the cross-origin iframe.

Note: These findings are based on static source code analysis. Our tooling currently lacks the ability to execute live exploit code; therefore, the reproduction steps and potential impact described below represent a theoretical vulnerability trace based on verified code paths.


Root Cause Analysis

  1. OnQuery unconditionally updates last_query_ without dismissing active UI (components/autofill/core/browser/ui/autofill_external_delegate.cc:430-441): When a renderer calls mojom::AutofillDriver::AskForValuesToFill, BrowserAutofillManager::OnAskForValuesToFillImpl calls external_delegate_->OnQuery(...), which unconditionally sets last_query_ to the newly queried form and field ID. OnQuery does not hide or check consistency against an already open popup.

  2. Silent dropping in OnSuggestionsReturned (components/autofill/core/browser/ui/autofill_external_delegate.cc:462-469): When asynchronous suggestion generation completes for an earlier field, OnSuggestionsReturned checks:

    if (trigger_field.global_id() != last_query_.field_id) {
      return;
    }
    

    Unlike AttemptToDisplayAutofillSuggestions (autofill_external_delegate.cc:509-514), which calls HideSuggestions(SuggestionHidingReason::kStaleData), OnSuggestionsReturned executes a bare return;, leaving the current popup displayed.

  3. Asynchronous/OTP suggestions produce no new UI events (components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc:98-101): When querying a field classified with autocomplete="section-p one-time-code", OtpManagerImpl::GetOtpSuggestions stores the Phase 2 suggestion callback in last_pending_get_suggestions_callback_. On desktop platforms, ChromeAutofillClient::GetOneTimeTokenService() returns nullptr (chrome/browser/ui/autofill/chrome_autofill_client.cc:1464), causing GetRecentOtpsAndRenewSubscription() to return immediately without invoking the callback or requesting UI display. No new ShowAutofillSuggestions task is posted, so show-time checks (kAutofillCheckTriggeringFieldDoesNotChangeDuringFilling) are not invoked.

  4. Accept path fills via last_query_ (chrome/browser/ui/autofill/autofill_popup_controller_impl.cc:552-594 and components/autofill/core/browser/ui/autofill_external_delegate.cc:1518-1520): When the user selects a suggestion, AutofillPopupControllerImpl::AcceptSuggestion forwards the event to delegate_->DidAcceptSuggestion -> DidAcceptPaymentsSuggestion -> AutofillForm -> payments::FillOrPreviewCard, which passes last_query_.form_id and last_query_.field_id to manager_->FillOrPreviewForm.

  5. Bypass of IsSafeToFill origin boundary (components/autofill/core/browser/foundations/autofill_driver_router.cc:87-124): AutofillDriverRouter::ApplyFormAction validates whether fields are safe to fill via IsSafeToFill(field, type, main_origin, trigger_origin). Because trigger_origin is derived from trigger_field.origin() (last_query_, belonging to the attacker), field.origin() == trigger_origin evaluates to true for all attacker fields in the same section (section-p), allowing sensitive credit card data to be dispatched to the attacker’s renderer.


Potential Trigger Steps

  1. An attacker page at https://attacker.com (Main Frame $A$, running in a compromised renderer) renders a <form> containing:
    • Visually hidden credit card fields declared with autocomplete="section-p cc-number", cc-name, cc-exp, cc-csc.
    • Text fields $A_1$ and $A_2$ declared with autocomplete="section-p one-time-code".
    • An embedded checkout iframe https://merchant.com (Frame $M$) with legitimate payment inputs ($M_{cc}$).
  2. Form structure processing in FormForest flattens Frame $M$’s fields into Frame $A$’s root browser form, assigning all fields sharing autocomplete="section-p ..." into section-p.
  3. The victim clicks $M_{cc}$. The ask routes to the root BrowserAutofillManager, setting last_query_ = M_{cc} and displaying the saved payment card popup over $M_{cc}$.
  4. Frame $A$’s renderer calls mojom::AutofillDriver::AskForValuesToFill for field $A_1$ (kFormControlElementClicked).
  5. OnQuery overwrites last_query_ = A_1 without dismissing the popup. On desktop, the OTP manager parks the callback without generating UI updates.
  6. The victim clicks their saved card in the still-open popup.
  7. The browser fills credit card data using last_query_ ($A_1$), matching section section-p. Frame $A$’s CC fields are populated and dispatched via mojom::AutofillAgent::ApplyFieldsAction to Frame $A$’s renderer.

Suggested Fix

  1. Enforce accept-time verification: In AutofillPopupControllerImpl::AcceptSuggestion or AutofillExternalDelegate::DidAcceptSuggestion, verify that the currently queried field (last_query_.field_id) matches the field ID for which the suggestion popup was originally opened before executing fill actions.
  2. Dismiss popup on stale results: Update AutofillExternalDelegate::OnSuggestionsReturned (autofill_external_delegate.cc:467-469) to call manager_->client().HideSuggestions(SuggestionHidingReason::kStaleData) instead of performing a silent bare return when trigger_field.global_id() != last_query_.field_id.
  3. Dismiss popup on unhandled queries: In BrowserAutofillManager::OnAskForValuesToFillImpl or OnQuery, explicitly dismiss or update existing popups when last_query_ changes to a different form/field.

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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