Overview

Medium
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Transactions Platform
DescriptionMissing authorization in Transactions Platform
ComponentTransactions Platform
Bug ClassAuthorization Error
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.
Tracker521951328
Fix commit343058f4edce (chromium/src) +84/-20
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-06-09 (per vendor advisory)
Fix landed2026-09-11 (public git)
Disclosed2026-09-15
Patch gap4 days — fix public in source before the advisory

Background

`UpdateAutofillDataListValues()`
An AutofillClient method that pushes updated <datalist> options into the currently visible Autofill suggestion popup.
`LocalFrameToken`
An unguessable token that uniquely identifies a single renderer frame, used browser-side to authorize per-frame actions.
`suggestion_controller_`
The single per-WebContents controller that owns the active Autofill popup and is associated with exactly one frame at a time via controller_common_.frame_token.
`AutofillExternalDelegate`
The per-frame, browser-side object that handles Autofill queries and relays renderer-supplied field data (including datalist options) to the AutofillClient.

Root Cause Analysis

Pre-patch, ChromeAutofillClient::UpdateAutofillDataListValues gated the update solely on suggestion_controller_ being non-null, and then unconditionally called suggestion_controller_->UpdateDataListValues(options). Because one ChromeAutofillClient and its suggestion_controller_ are shared across every frame in a WebContents, a datalist update that originated in one frame’s AutofillExternalDelegate would be applied to a popup that a different frame had opened. The violated invariant is that only the frame that owns the currently-shown popup may mutate its contents; nothing tied the caller’s frame identity to the controller’s owning frame.

The fix threads the querying field’s frame token (field.global_id().frame_token) down through the AutofillClient::UpdateAutofillDataListValues interface and compares it against the new accessor suggestion_controller_->GetFrameToken(), applying the update only when the tokens match. This adds the missing per-frame authorization check, so the condition is TIGHTENED from “a popup exists” to “a popup exists and the caller owns it.”

Key insight
The guard tested only whether a popup existed, not whether the requesting frame owned it; the fix adds a LocalFrameToken equality check so a frame can update datalist values only in its own popup.

Attack Path

  1. Victim popup is open The user focuses a field in frame B, and Autofill shows a suggestion/datalist popup owned by the shared suggestion_controller_, which is associated with frame B’s token.
  2. Attacker frame issues a query A malicious or compromised renderer frame A (e.g. a cross-origin subframe in the same tab) sends an Autofill query carrying attacker-chosen <datalist> options through frame A’s AutofillExternalDelegate.
  3. Unauthorized cross-frame update Pre-patch ChromeAutofillClient::UpdateAutofillDataListValues sees suggestion_controller_ is non-null and applies frame A’s datalist options to frame B’s popup without any frame-token check.
  4. Injected content in the victim popup The popup shown for frame B now displays attacker-controlled datalist entries, enabling cross-frame content injection/spoofing (inferred: could be leveraged to mislead the user or desynchronize popup state such as the selected line index).

Impact Assessment

An attacker controlling one renderer frame can inject or replace the datalist options displayed in an Autofill popup that belongs to a different frame in the same WebContents, acting in the browser-process UI, with the sole precondition that a popup is currently showing for the victim frame. This is a missing-authorization logic bug (no memory corruption is demonstrated by the patch) that permits cross-frame UI content injection/spoofing. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.2. Any escalation to credential or data disclosure is (inferred).

Changed Functions

FunctionChangeNotes
WebContents
chrome/browser/ui/autofill/autofill_suggestion_controller.h
modified
if
chrome/browser/ui/autofill/chrome_autofill_client.cc
modified
TEST_F
chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
modified
TEST_F
components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
modified

Files Changed

  • chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
  • chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
  • components/android_autofill/browser/android_autofill_client.h
  • ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.h
  • chrome/browser/ui/autofill/mock_autofill_popup_controller.h
  • chrome/browser/ui/autofill/chrome_autofill_client.cc
  • ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.mm
  • ios/web_view/internal/autofill/web_view_autofill_client_ios.mm
  • components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
  • components/android_autofill/browser/android_autofill_client.cc
  • chrome/browser/ui/autofill/at_memory_suggestion_controller.h
  • components/autofill/core/browser/foundations/test_autofill_client.h
  • chrome/browser/ui/autofill/autofill_suggestion_controller.h
  • chrome/browser/ui/autofill/autofill_popup_controller_impl.h
  • ios/web_view/internal/autofill/web_view_autofill_client_ios.h
  • chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc
  • chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.h
  • chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
  • chrome/browser/ui/autofill/chrome_autofill_client.h
  • chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
  • components/autofill/core/browser/ui/autofill_external_delegate.cc
  • components/autofill/core/browser/foundations/autofill_client.h

Audit Directions

  • Shared per-`WebContents` singletons driven by per-frame IPC
    Any browser-side object keyed to a WebContents but mutated by messages from individual frames should verify the caller’s LocalFrameToken before acting.
  • UI mutators gated on existence, not ownership
    Methods that modify the currently-shown popup/UI element based only on “does one exist” (e.g. a non-null controller) rather than “does the caller own it” are prime candidates for cross-frame injection.
  • Frame-token plumbing gaps in Autofill entry points
    Audit AutofillClient/AutofillManager methods that apply renderer-supplied field data without threading field.global_id().frame_token, since datalist was one of several such paths.
From 343058f4edcefa3434a16466ce8eb1e1664bbb42 Mon Sep 17 00:00:00 2001
From: Jihad Hanna <jihadghanna@google.com>
Date: Fri, 11 Sep 2026 03:11:18 -0700
Subject: [PATCH] Validate calls to UpdateAutofillDataListValues()

Details in the bug description. The CL compares frame tokens instead of
delegate pointers as it seems like the more direct comparison to make.

Fixed: 521951328
Change-Id: Ib0537a3e941c0a744248e3f931253ee378a35fd1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8367287
Reviewed-by: Jan Keitel <jkeitel@google.com>
Reviewed-by: Christoph Schwering <schwering@google.com>
Commit-Queue: Jihad Hanna <jihadghanna@google.com>
Cr-Commit-Position: refs/heads/main@{#1696085}
---
diff --git a/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc b/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
index 358176d2..b383ef9e 100644
--- a/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
+++ b/chrome/browser/ui/autofill/at_memory_suggestion_controller.cc
@@ -169,6 +169,10 @@
   NOTREACHED();
 }
 
+const LocalFrameToken& AtMemorySuggestionController::GetFrameToken() const {
+  return controller_common_.frame_token;
+}
+
 void AtMemorySuggestionController::HideViewAndDie() {
   ui_session_id_ = std::nullopt;
 
diff --git a/chrome/browser/ui/autofill/at_memory_suggestion_controller.h b/chrome/browser/ui/autofill/at_memory_suggestion_controller.h
index f417fdd..21fa9a94 100644
--- a/chrome/browser/ui/autofill/at_memory_suggestion_controller.h
+++ b/chrome/browser/ui/autofill/at_memory_suggestion_controller.h
@@ -69,6 +69,7 @@
   std::optional<UiSessionId> GetUiSessionId() const override;
   void SetKeepPopupOpenForTesting(bool keep_popup_open_for_testing) override;
   void UpdateDataListValues(base::span<const SelectOption> options) override;
+  const LocalFrameToken& GetFrameToken() const override;
   bool MayRecycle(
       base::WeakPtr<AutofillSuggestionDelegate> delegate,
       content::WebContents* web_contents,
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 eae42c1c..56de103 100644
--- a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
+++ b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.cc
@@ -755,6 +755,11 @@
   }
 }
 
+const LocalFrameToken& AutofillKeyboardAccessoryControllerImpl::GetFrameToken()
+    const {
+  return controller_common_.frame_token;
+}
+
 bool AutofillKeyboardAccessoryControllerImpl::HasSuggestions() const {
   return std::ranges::any_of(suggestions_, &IsStandaloneSuggestionType,
                              &Suggestion::type);
diff --git a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.h b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.h
index 3d2ff87b..ebccecb 100644
--- a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.h
+++ b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.h
@@ -94,6 +94,7 @@
   std::optional<UiSessionId> GetUiSessionId() const override;
   void SetKeepPopupOpenForTesting(bool keep_popup_open_for_testing) override;
   void UpdateDataListValues(base::span<const SelectOption> options) override;
+  const LocalFrameToken& GetFrameToken() const override;
   bool MayRecycle(
       base::WeakPtr<AutofillSuggestionDelegate> delegate,
       content::WebContents* web_contents,
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc b/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
index 8c417aa..6e15018f 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_impl.cc
@@ -483,6 +483,10 @@
   }
 }
 
+const LocalFrameToken& AutofillPopupControllerImpl::GetFrameToken() const {
+  return controller_common_.frame_token;
+}
+
 bool AutofillPopupControllerImpl::IsViewVisibilityAcceptingThresholdEnabled()
     const {
   return !disable_threshold_for_testing_ &&
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_impl.h b/chrome/browser/ui/autofill/autofill_popup_controller_impl.h
index e463460..e60b4d1e 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_impl.h
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_impl.h
@@ -74,6 +74,7 @@
   std::optional<UiSessionId> GetUiSessionId() const override;
   void SetKeepPopupOpenForTesting(bool keep_popup_open_for_testing) override;
   void UpdateDataListValues(base::span<const SelectOption> options) override;
+  const LocalFrameToken& GetFrameToken() const override;
   bool MayRecycle(
       base::WeakPtr<AutofillSuggestionDelegate> delegate,
       content::WebContents* web_contents,
diff --git a/chrome/browser/ui/autofill/autofill_suggestion_controller.h b/chrome/browser/ui/autofill/autofill_suggestion_controller.h
index 336b6ac..998a370 100644
--- a/chrome/browser/ui/autofill/autofill_suggestion_controller.h
+++ b/chrome/browser/ui/autofill/autofill_suggestion_controller.h
@@ -17,6 +17,7 @@
 #include "components/autofill/core/browser/metrics/autofill_metrics.h"
 #include "components/autofill/core/browser/suggestions/suggestion.h"
 #include "components/autofill/core/common/aliases.h"
+#include "components/autofill/core/common/unique_ids.h"
 
 namespace content {
 class WebContents;
@@ -106,6 +107,9 @@
   // Updates the data list values currently shown.
   virtual void UpdateDataListValues(base::span<const SelectOption> options) = 0;
 
+  // Returns the token of the frame that this controller is associated with.
+  virtual const LocalFrameToken& GetFrameToken() const = 0;
+
   // Returns true if the controller can be reused for the given parameters.
   virtual bool MayRecycle(
       base::WeakPtr<AutofillSuggestionDelegate> delegate,
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.cc b/chrome/browser/ui/autofill/chrome_autofill_client.cc
index 7cc2d3d..ff84ec64 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.cc
@@ -908,8 +908,10 @@
 }
 
 void ChromeAutofillClient::UpdateAutofillDataListValues(
+    const LocalFrameToken& frame_token,
     base::span<const SelectOption> options) {
-  if (suggestion_controller_) {
+  if (suggestion_controller_ &&
+      suggestion_controller_->GetFrameToken() == frame_token) {
     suggestion_controller_->UpdateDataListValues(options);
   }
 }
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.h b/chrome/browser/ui/autofill/chrome_autofill_client.h
index ea61deb..b7001aa 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.h
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.h
@@ -211,6 +211,7 @@
       const PopupOpenArgs& open_args,
       base::WeakPtr<AutofillSuggestionDelegate> delegate) override;
   void UpdateAutofillDataListValues(
+      const LocalFrameToken& frame_token,
       base::span<const SelectOption> datalist) final;
   base::span<const Suggestion> GetAutofillSuggestions() const final;
   std::optional<SuggestionUiSessionId>
diff --git a/chrome/browser/ui/autofill/mock_autofill_popup_controller.h b/chrome/browser/ui/autofill/mock_autofill_popup_controller.h
index 89aac74..7de6c73 100644
--- a/chrome/browser/ui/autofill/mock_autofill_popup_controller.h
+++ b/chrome/browser/ui/autofill/mock_autofill_popup_controller.h
@@ -52,6 +52,10 @@
   void set_element_bounds(const gfx::RectF& bounds) {
     element_bounds_ = bounds;
   }
+  const LocalFrameToken& GetFrameToken() const override { return frame_token_; }
+  void set_frame_token(const LocalFrameToken& frame_token) {
+    frame_token_ = frame_token;
+  }
   MOCK_METHOD(base::i18n::TextDirection,
               GetElementTextDirection,
               (),
@@ -166,6 +170,7 @@
       "Arial, Times New Roman, 15px"};
   gfx::RectF element_bounds_ = {100, 100, 250, 50};
   PopupAnchorType anchor_type_ = PopupAnchorType::kField;
+  LocalFrameToken frame_token_;
 
   base::WeakPtrFactory<MockAutofillPopupController> weak_ptr_factory_{this};
 };
diff --git a/components/android_autofill/browser/android_autofill_client.cc b/components/android_autofill/browser/android_autofill_client.cc
index c3b4fa0a..feba6818 100644
--- a/components/android_autofill/browser/android_autofill_client.cc
+++ b/components/android_autofill/browser/android_autofill_client.cc
@@ -233,6 +233,7 @@
 }
 
 void AndroidAutofillClient::UpdateAutofillDataListValues(
+    const autofill::LocalFrameToken& frame_token,
     base::span<const autofill::SelectOption> datalist) {
   // Leaving as an empty method since updating autofill popup window
   // dynamically does not seem to be a useful feature when delegating to Android
diff --git a/components/android_autofill/browser/android_autofill_client.h b/components/android_autofill/browser/android_autofill_client.h
index 4e5187bf..4c324a0e 100644
--- a/components/android_autofill/browser/android_autofill_client.h
+++ b/components/android_autofill/browser/android_autofill_client.h
@@ -127,6 +127,7 @@
       const autofill::AutofillClient::PopupOpenArgs& open_args,
       base::WeakPtr<autofill::AutofillSuggestionDelegate> delegate) final;
   void UpdateAutofillDataListValues(
+      const autofill::LocalFrameToken& frame_token,
       base::span<const autofill::SelectOption> datalist) final;
   void HideSuggestions(autofill::SuggestionHidingReason reason,
                        std::optional<autofill::FillingProduct> product) final;
diff --git a/components/autofill/core/browser/foundations/autofill_client.h b/components/autofill/core/browser/foundations/autofill_client.h
index 37965a54..69e8ff9 100644
--- a/components/autofill/core/browser/foundations/autofill_client.h
+++ b/components/autofill/core/browser/foundations/autofill_client.h
@@ -665,6 +665,7 @@
 
   // Update the data list values shown by the Autofill suggestions, if visible.
   virtual void UpdateAutofillDataListValues(
+      const LocalFrameToken& frame_token,
       base::span<const SelectOption> datalist) = 0;
 
   // Returns the identifier of the suggestion UI that is currently showing or
diff --git a/components/autofill/core/browser/foundations/test_autofill_client.h b/components/autofill/core/browser/foundations/test_autofill_client.h
index 91b442d..f1e3bc23 100644
--- a/components/autofill/core/browser/foundations/test_autofill_client.h
+++ b/components/autofill/core/browser/foundations/test_autofill_client.h
@@ -410,6 +410,7 @@
   }
 
   void UpdateAutofillDataListValues(
+      const LocalFrameToken& frame_token,
       base::span<const SelectOption> options) override {}
 
   void SetAutofillSuggestions(std::vector<Suggestion> suggestions) {
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate.cc b/components/autofill/core/browser/ui/autofill_external_delegate.cc
index 3a840a9d1..12756da 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate.cc
@@ -438,7 +438,8 @@
                  .field_datalist_options = field.datalist_options()};
   caret_bounds_ = caret_bounds;
   trigger_source_ = trigger_source;
-  manager_->client().UpdateAutofillDataListValues(field.datalist_options());
+  manager_->client().UpdateAutofillDataListValues(field.global_id().frame_token,
+                                                  field.datalist_options());
 }
 
 const AutofillField* AutofillExternalDelegate::GetQueriedField() const {
diff --git a/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.h b/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.h
index 9a7b19d..d911716 100644
--- a/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.h
+++ b/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.h
@@ -171,6 +171,7 @@
       const PopupOpenArgs& open_args,
       base::WeakPtr<AutofillSuggestionDelegate> delegate) override;
   void UpdateAutofillDataListValues(
+      const LocalFrameToken& frame_token,
       base::span<const SelectOption> datalist) override;
   void HideSuggestions(SuggestionHidingReason reason,
                        std::optional<FillingProduct> product) override;
diff --git a/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.mm b/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.mm
index ee508dc..7cd3365ad 100644
--- a/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.mm
@@ -547,6 +547,7 @@
 }
 
 void ChromeAutofillClientIOS::UpdateAutofillDataListValues(
+    const LocalFrameToken& frame_token,
     base::span<const SelectOption> datalist) {
   // No op. ios/web_view does not support display datalist.
 }
diff --git a/ios/web_view/internal/autofill/web_view_autofill_client_ios.h b/ios/web_view/internal/autofill/web_view_autofill_client_ios.h
index a8ce175..e49b3c3 100644
--- a/ios/web_view/internal/autofill/web_view_autofill_client_ios.h
+++ b/ios/web_view/internal/autofill/web_view_autofill_client_ios.h
@@ -133,6 +133,7 @@
       const AutofillClient::PopupOpenArgs& open_args,
       base::WeakPtr<AutofillSuggestionDelegate> delegate) override;
   void UpdateAutofillDataListValues(
+      const LocalFrameToken& frame_token,
       base::span<const SelectOption> datalist) override;
   void HideSuggestions(SuggestionHidingReason reason,
                        std::optional<FillingProduct> product) override;
diff --git a/ios/web_view/internal/autofill/web_view_autofill_client_ios.mm b/ios/web_view/internal/autofill/web_view_autofill_client_ios.mm
index af6dfaf..5c030bb 100644
--- a/ios/web_view/internal/autofill/web_view_autofill_client_ios.mm
+++ b/ios/web_view/internal/autofill/web_view_autofill_client_ios.mm
@@ -251,6 +251,7 @@
 }
 
 void WebViewAutofillClientIOS::UpdateAutofillDataListValues(
+    const LocalFrameToken& frame_token,
     base::span<const SelectOption> datalist) {
   // No op. ios/web_view does not support display datalist.
 }
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc b/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc
index baac8ab8a..71171c81 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc
@@ -220,13 +220,13 @@
                                     {u"suggestion2", u"suggestion2"},
                                     {u"suggestion3", u"suggestion3"},
                                     {u"suggestion4", u"suggestion4"}};
-  client->UpdateAutofillDataListValues(rows);
+  client->UpdateAutofillDataListValues(controller->GetFrameToken(), rows);
   int original_suggestions_count = controller->GetLineCount();
   static_cast<AutofillPopupController&>(*controller).SelectSuggestion(3);
 
   // Replace the list with the smaller one.
   rows = {{u"suggestion1", u"suggestion1"}};
-  client->UpdateAutofillDataListValues(rows);
+  client->UpdateAutofillDataListValues(controller->GetFrameToken(), rows);
   // Make sure that previously selected line #3 doesn't exist.
   ASSERT_LT(controller->GetLineCount(), original_suggestions_count);
   // Selecting a new line should not crash.
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
index c933d3d..427c8ba 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
@@ -57,6 +57,7 @@
 #include "components/autofill/core/common/autofill_prefs.h"
 #include "components/autofill/core/common/autofill_test_util.h"
 #include "components/autofill/core/common/form_field_data.h"
+#include "components/autofill/core/common/unique_ids.h"
 #include "components/feature_engagement/public/feature_constants.h"
 #include "components/personal_context/core/personal_context_eligibility_service.h"
 #include "components/personal_context/core/personal_context_prefs.h"
@@ -1275,6 +1276,30 @@
   client()->HideSuggestions(SuggestionHidingReason::kAcceptSuggestion,
                             FillingProduct::kAddress);
 }
+
+TEST_F(ChromeAutofillClientTest,
+       UpdateAutofillDataListValues_FrameTokenFilter) {
+  LocalFrameToken token1(base::UnguessableToken::Create());
+  LocalFrameToken token2(base::UnguessableToken::Create());
+
+  testing::NiceMock<MockAutofillPopupController> mock_controller;
+  mock_controller.set_frame_token(token1);
+
+  client()->set_suggestion_controller_for_testing(mock_controller.GetWeakPtr());
+
+  std::vector<SelectOption> options = {{.value = u"val", .text = u"txt"}};
+
+  // Attempt to update with a non-matching frame token should be ignored.
+  EXPECT_CALL(mock_controller, UpdateDataListValues).Times(0);
+  client()->UpdateAutofillDataListValues(token2, options);
+  testing::Mock::VerifyAndClearExpectations(&mock_controller);
+
+  // Attempt to update with a matching frame token should succeed.
+  EXPECT_CALL(
+      mock_controller,
+      UpdateDataListValues(ElementsAre(Field(&SelectOption::value, u"val"))));
+  client()->UpdateAutofillDataListValues(token1, options);
+}
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 TEST_F(ChromeAutofillClientTest, IsAutofillProfileEnabled_BlockedByPolicy) {
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
index 68c6b82..653998c 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
@@ -280,7 +280,7 @@
               (const override));
   MOCK_METHOD(void,
               UpdateAutofillDataListValues,
-              (base::span<const SelectOption> options),
+              (const LocalFrameToken&, base::span<const SelectOption> options),
               (override));
   MOCK_METHOD(void,
               HideSuggestions,
@@ -1580,7 +1580,7 @@
   std::vector<SelectOption> data_list_items;
   data_list_items.emplace_back();
 
-  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(SizeIs(1)));
+  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(_, SizeIs(1)));
   IssueOnQuery(data_list_items);
 
   // This should call ShowAutofillSuggestions.
@@ -1616,7 +1616,7 @@
   std::vector<SelectOption> data_list_items;
   data_list_items.emplace_back();
 
-  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(SizeIs(1)));
+  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(_, SizeIs(1)));
   IssueOnQuery(data_list_items);
 
   // Ensure the popup is displayed.
@@ -1640,7 +1640,7 @@
   // Update the current data list and ensure the popup is updated.
   data_list_items.emplace_back();
 
-  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(SizeIs(2)));
+  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(_, SizeIs(2)));
   IssueOnQuery(data_list_items);
 }
 
@@ -1649,12 +1649,13 @@
 TEST_F(AutofillExternalDelegateTest, DuplicateAutofillDatalistValues) {
   std::vector<SelectOption> datalist{{.value = u"Rick", .text = u"Deckard"},
                                      {.value = u"Beyonce", .text = u"Knowles"}};
-  EXPECT_CALL(autofill_client(),
-              UpdateAutofillDataListValues(
-                  ElementsAre(AllOf(Field(&SelectOption::value, u"Rick"),
-                                    Field(&SelectOption::text, u"Deckard")),
-                              AllOf(Field(&SelectOption::value, u"Beyonce"),
-                                    Field(&SelectOption::text, u"Knowles")))));
+  EXPECT_CALL(
+      autofill_client(),
+      UpdateAutofillDataListValues(
+          _, ElementsAre(AllOf(Field(&SelectOption::value, u"Rick"),
+                               Field(&SelectOption::text, u"Deckard")),
+                         AllOf(Field(&SelectOption::value, u"Beyonce"),
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc b/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc
index baac8ab8a..71171c81 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_interactive_uitest.cc
@@ -220,13 +220,13 @@
                                     {u"suggestion2", u"suggestion2"},
                                     {u"suggestion3", u"suggestion3"},
                                     {u"suggestion4", u"suggestion4"}};
-  client->UpdateAutofillDataListValues(rows);
+  client->UpdateAutofillDataListValues(controller->GetFrameToken(), rows);
   int original_suggestions_count = controller->GetLineCount();
   static_cast<AutofillPopupController&>(*controller).SelectSuggestion(3);
 
   // Replace the list with the smaller one.
   rows = {{u"suggestion1", u"suggestion1"}};
-  client->UpdateAutofillDataListValues(rows);
+  client->UpdateAutofillDataListValues(controller->GetFrameToken(), rows);
   // Make sure that previously selected line #3 doesn't exist.
   ASSERT_LT(controller->GetLineCount(), original_suggestions_count);
   // Selecting a new line should not crash.
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
index c933d3d..427c8ba 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client_unittest.cc
@@ -57,6 +57,7 @@
 #include "components/autofill/core/common/autofill_prefs.h"
 #include "components/autofill/core/common/autofill_test_util.h"
 #include "components/autofill/core/common/form_field_data.h"
+#include "components/autofill/core/common/unique_ids.h"
 #include "components/feature_engagement/public/feature_constants.h"
 #include "components/personal_context/core/personal_context_eligibility_service.h"
 #include "components/personal_context/core/personal_context_prefs.h"
@@ -1275,6 +1276,30 @@
   client()->HideSuggestions(SuggestionHidingReason::kAcceptSuggestion,
                             FillingProduct::kAddress);
 }
+
+TEST_F(ChromeAutofillClientTest,
+       UpdateAutofillDataListValues_FrameTokenFilter) {
+  LocalFrameToken token1(base::UnguessableToken::Create());
+  LocalFrameToken token2(base::UnguessableToken::Create());
+
+  testing::NiceMock<MockAutofillPopupController> mock_controller;
+  mock_controller.set_frame_token(token1);
+
+  client()->set_suggestion_controller_for_testing(mock_controller.GetWeakPtr());
+
+  std::vector<SelectOption> options = {{.value = u"val", .text = u"txt"}};
+
+  // Attempt to update with a non-matching frame token should be ignored.
+  EXPECT_CALL(mock_controller, UpdateDataListValues).Times(0);
+  client()->UpdateAutofillDataListValues(token2, options);
+  testing::Mock::VerifyAndClearExpectations(&mock_controller);
+
+  // Attempt to update with a matching frame token should succeed.
+  EXPECT_CALL(
+      mock_controller,
+      UpdateDataListValues(ElementsAre(Field(&SelectOption::value, u"val"))));
+  client()->UpdateAutofillDataListValues(token1, options);
+}
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 TEST_F(ChromeAutofillClientTest, IsAutofillProfileEnabled_BlockedByPolicy) {
diff --git a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
index 68c6b82..653998c 100644
--- a/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
+++ b/components/autofill/core/browser/ui/autofill_external_delegate_unittest.cc
@@ -280,7 +280,7 @@
               (const override));
   MOCK_METHOD(void,
               UpdateAutofillDataListValues,
-              (base::span<const SelectOption> options),
+              (const LocalFrameToken&, base::span<const SelectOption> options),
               (override));
   MOCK_METHOD(void,
               HideSuggestions,
@@ -1580,7 +1580,7 @@
   std::vector<SelectOption> data_list_items;
   data_list_items.emplace_back();
 
-  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(SizeIs(1)));
+  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(_, SizeIs(1)));
   IssueOnQuery(data_list_items);
 
   // This should call ShowAutofillSuggestions.
@@ -1616,7 +1616,7 @@
   std::vector<SelectOption> data_list_items;
   data_list_items.emplace_back();
 
-  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(SizeIs(1)));
+  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(_, SizeIs(1)));
   IssueOnQuery(data_list_items);
 
   // Ensure the popup is displayed.
@@ -1640,7 +1640,7 @@
   // Update the current data list and ensure the popup is updated.
   data_list_items.emplace_back();
 
-  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(SizeIs(2)));
+  EXPECT_CALL(autofill_client(), UpdateAutofillDataListValues(_, SizeIs(2)));
   IssueOnQuery(data_list_items);
 }
 
@@ -1649,12 +1649,13 @@
 TEST_F(AutofillExternalDelegateTest, DuplicateAutofillDatalistValues) {
   std::vector<SelectOption> datalist{{.value = u"Rick", .text = u"Deckard"},
                                      {.value = u"Beyonce", .text = u"Knowles"}};
-  EXPECT_CALL(autofill_client(),
-              UpdateAutofillDataListValues(
-                  ElementsAre(AllOf(Field(&SelectOption::value, u"Rick"),
-                                    Field(&SelectOption::text, u"Deckard")),
-                              AllOf(Field(&SelectOption::value, u"Beyonce"),
-                                    Field(&SelectOption::text, u"Knowles")))));
+  EXPECT_CALL(
+      autofill_client(),
+      UpdateAutofillDataListValues(
+          _, ElementsAre(AllOf(Field(&SelectOption::value, u"Rick"),
+                               Field(&SelectOption::text, u"Deckard")),
+                         AllOf(Field(&SelectOption::value, u"Beyonce"),
+                               Field(&SelectOption::text, u"Knowles")))));
   IssueOnQuery(datalist);
 
   const auto kExpectedSuggestions = SuggestionVectorIdsAre(
@@ -1681,12 +1682,13 @@
 TEST_F(AutofillExternalDelegateTest, DuplicateAutocompleteDatalistValues) {
   std::vector<SelectOption> datalist{{.value = u"Rick", .text = u"Deckard"},
                                      {.value = u"Beyonce", .text = u"Knowles"}};
-  EXPECT_CALL(autofill_client(),
-              UpdateAutofillDataListValues(
-                  ElementsAre(AllOf(Field(&SelectOption::value, u"Rick"),
-                                    Field(&SelectOption::text, u"Deckard")),
-                              AllOf(Field(&SelectOption::value, u"Beyonce"),
-                                    Field(&SelectOption::text, u"Knowles")))));
+  EXPECT_CALL(
+      autofill_client(),
+      UpdateAutofillDataListValues(
+          _, ElementsAre(AllOf(Field(&SelectOption::value, u"Rick"),
+                               Field(&SelectOption::text, u"Deckard")),
+                         AllOf(Field(&SelectOption::value, u"Beyonce"),
+                               Field(&SelectOption::text, u"Knowles")))));
   IssueOnQuery(datalist);
 
   const auto kExpectedSuggestions = SuggestionVectorIdsAre(
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.