Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncomplete cleanup in Autofill
DescriptionIncomplete cleanup in Autofill
ComponentAutofill
Sub-componentAutofill
Bug ClassLogic Error
Tracker553394296
Fix commit08e17e0a8b2b (chromium/src) +772/-54
CISA KEVNot listed
CreditedAnonymous
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm
modified
WebStateList
ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.h
modified
if
ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mm
modified

Files Changed

  • ios/chrome/browser/autofill/payments/coordinator/payments_suggestion_bottom_sheet_coordinator_unittest.mm
  • ios/chrome/browser/autofill/payments/coordinator/payments_suggestion_bottom_sheet_coordinator.mm
  • tools/metrics/histograms/metadata/ios/enums.xml
  • ios/chrome/browser/autofill/payments/test/BUILD.gn
  • ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mm
  • ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h
  • ios/chrome/browser/autofill/payments/coordinator/BUILD.gn
  • ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.h
  • ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator_unittest.mm
  • ios/chrome/browser/autofill/payments/coordinator/payments_suggestion_bottom_sheet_exit_reason.h
  • ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm
  • ios/chrome/browser/autofill/payments/test/payments_suggestion_bottom_sheet_egtest.mm
  • ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper_unittest.mm
  • ios/chrome/browser/autofill/model/bottom_sheet/BUILD.gn
From 08e17e0a8b2bf47a408a2ae11a16c74205eab7d9 Mon Sep 17 00:00:00 2001
From: Vincent Boisselle <[email protected]>
Date: Wed, 23 Sep 2026 07:13:05 -0700
Subject: [PATCH] [Autofill] Invalidate payments bottom sheet on WebState navigation

Invalidate the payments bottom sheet fill context and cancel in-flight
suggestion requests when the active WebState undergoes cross-document
navigation or tab switching.

This ensures suggestion callbacks and selection events cannot rebind or
trigger autofill / element refocusing on newly loaded documents.

NO_IFTTT=ifttt isn't correctly set in the enum.xml file, I confirm we did sync that file with the enum change in the source file

Fixed: 553394296
Change-Id: I2e35aaa4d3e880a5c9696ff7845b1efc6df64cd6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8427322
Reviewed-by: Tommy Martino <[email protected]>
Commit-Queue: Vincent Boisselle <[email protected]>
Reviewed-by: Ginny Huang <[email protected]>
Auto-Submit: Vincent Boisselle <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1703369}
---
diff --git a/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h b/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h
index 8d048682..dfc2442 100644
--- a/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h
+++ b/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h
@@ -7,6 +7,7 @@
 
 #import <memory>
 
+#import "base/gtest_prod_util.h"
 #import "base/memory/raw_ptr.h"
 #import "base/memory/weak_ptr.h"
 #import "base/scoped_multi_source_observation.h"
@@ -95,7 +96,7 @@
   // Handler for JavaScript messages. Dispatch to more specific handler.
   void OnFormMessageReceived(const web::ScriptMessage& message);
 
-  // Sets the bottom sheet CommandDispatcher.
+  // Sets the bottom sheet AutofillCommands handler.
   void SetAutofillBottomSheetHandler(id<AutofillCommands> commands_handler);
 
   // Sets the password generation provider used for proactive password
@@ -148,8 +149,11 @@
   void RefocusElementIfNeeded(const std::string& frame_id);
 
   // WebStateObserver:
+  void DidStartNavigation(web::WebState* web_state,
+                          web::NavigationContext* navigation_context) override;
   void DidFinishNavigation(web::WebState* web_state,
                            web::NavigationContext* navigation_context) override;
+  void RenderProcessGone(web::WebState* web_state) override;
   void WebStateDestroyed(web::WebState* web_state) override;
 
   // web::WebFramesManager::Observer:
@@ -204,6 +208,10 @@
 
  private:
   friend class web::WebStateUserData<AutofillBottomSheetTabHelper>;
+  FRIEND_TEST_ALL_PREFIXES(AutofillBottomSheetTabHelperTest,
+                           DidStartNavigationCancelsPendingCallbacks);
+  FRIEND_TEST_ALL_PREFIXES(AutofillBottomSheetTabHelperTest,
+                           RenderProcessGoneClearsState);
 
   explicit AutofillBottomSheetTabHelper(web::WebState* web_state);
 
@@ -260,6 +268,10 @@
   void ShowProactivePasswordGenerationBottomSheet(
       const autofill::FormActivityParams& params);
 
+  // Cancels any pending asynchronous suggestion queries (e.g. for payments
+  // bottom sheet) associated with a previous page or document.
+  void CancelPendingSuggestionRequests();
+
   // Password generation provider used to trigger proactive password generation
   id<PasswordGenerationProvider> generation_provider_;
 
@@ -317,6 +329,13 @@
   // completed.
   autofill::VirtualCardEnrollmentCallbacks virtual_card_enrollment_callbacks_;
 
+  // Weak pointers handed out to pending suggestion retrieval callbacks. Kept
+  // separate from `weak_factory_` because `CancelPendingSuggestionRequests()`
+  // invalidates the whole factory, and only suggestion requests may be
+  // cancelled that way.
+  base::WeakPtrFactory<AutofillBottomSheetTabHelper>
+      suggestion_request_weak_factory_{this};
+
   base::WeakPtrFactory<AutofillBottomSheetTabHelper> weak_factory_{this};
 };
 
diff --git a/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm b/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm
index 3af5bf8c..7eaee93 100644
--- a/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm
+++ b/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm
@@ -211,9 +211,9 @@
 
 void AutofillBottomSheetTabHelper::SetAutofillBottomSheetHandler(
     id<AutofillCommands> commands_handler) {
-  if (!commands_handler) {
-    // Means that the web state has been destroyed therefore dismiss the edit
-    // address bottom sheet if it's shown.
+  if (!commands_handler && commands_handler_) {
+    // If the handler is being cleared, dismiss the edit address bottom sheet
+    // if it is currently shown.
     [commands_handler_ dismissEditAddressBottomSheet];
   }
   commands_handler_ = commands_handler;
@@ -328,10 +328,11 @@
   // the bottom sheet as a valid signal as a manual gesture.
   params.has_user_gesture = true;
 
-  auto completion = base::CallbackToBlock(base::BindOnce(
-      &AutofillBottomSheetTabHelper::
-          OnSuggestionsRetrievedForPaymentsBottomSheet,
-      weak_factory_.GetWeakPtr(), params, base::TimeTicks::Now()));
+  auto completion = base::CallbackToBlock(
+      base::BindOnce(&AutofillBottomSheetTabHelper::
+                         OnSuggestionsRetrievedForPaymentsBottomSheet,
+                     suggestion_request_weak_factory_.GetWeakPtr(), params,
+                     base::TimeTicks::Now()));
   [provider retrieveSuggestionsForForm:params
                               webState:web_state_
               accessoryViewUpdateBlock:completion];
@@ -617,16 +618,45 @@
                  web_state_->GetWeakPtr()));
 }
 
-// WebStateObserver
+void AutofillBottomSheetTabHelper::CancelPendingSuggestionRequests() {
+  suggestion_request_weak_factory_.InvalidateWeakPtrs();
+}
 
-void AutofillBottomSheetTabHelper::DidFinishNavigation(
+// WebStateObserver:
+
+void AutofillBottomSheetTabHelper::DidStartNavigation(
     web::WebState* web_state,
     web::NavigationContext* navigation_context) {
+  CHECK(navigation_context);
   if (navigation_context->IsSameDocument()) {
     return;
   }
 
-  // Clear all registered renderer ids
+  CancelPendingSuggestionRequests();
+}
+
+void AutofillBottomSheetTabHelper::DidFinishNavigation(
+    web::WebState* web_state,
+    web::NavigationContext* navigation_context) {
+  CHECK(navigation_context);
+  if (navigation_context->IsSameDocument() ||
+      !navigation_context->HasCommitted()) {
+    return;
+  }
+
+  CancelPendingSuggestionRequests();
+
+  // Clear all registered renderer IDs.
+  registered_password_renderer_ids_.clear();
+  registered_payments_renderer_ids_.clear();
+  registered_password_generation_renderer_ids_.clear();
+  registered_ambient_renderer_ids_.clear();
+}
+
+void AutofillBottomSheetTabHelper::RenderProcessGone(web::WebState* web_state) {
+  CancelPendingSuggestionRequests();
+
+  // Clear all registered renderer IDs.
   registered_password_renderer_ids_.clear();
   registered_payments_renderer_ids_.clear();
   registered_password_generation_renderer_ids_.clear();
@@ -634,6 +664,7 @@
 }
 
 void AutofillBottomSheetTabHelper::WebStateDestroyed(web::WebState* web_state) {
+  CancelPendingSuggestionRequests();
   web_state->RemoveObserver(this);
   frames_manager_observation_.Reset();
   web_state_ = nullptr;
diff --git a/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.h b/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.h
index daba3d4c..4a31dc289 100644
--- a/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.h
+++ b/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.h
@@ -19,6 +19,18 @@
 class WebStateList;
 
 @protocol CreditCardSuggestionBottomSheetConsumer;
+@class CreditCardSuggestionBottomSheetMediator;
+
+// Delegate for handling mediator lifecycle actions, such as requesting
+// dismissal.
+@protocol CreditCardSuggestionBottomSheetMediatorDelegate <NSObject>
+
+// Requests coordinator dismissal and complete teardown.
+- (void)creditCardSuggestionBottomSheetMediatorDidRequestDismissal:
+    (CreditCardSuggestionBottomSheetMediator*)mediator;
+
+@end
+
 // This mediator fetches a list suggestions to display in the bottom sheet.
 // It also manages filling the form when a suggestion is selected, as well
 // as showing the keyboard if requested when the bottom sheet is dismissed.
@@ -33,6 +45,10 @@
 // The bottom sheet suggestions consumer.
 @property(nonatomic, weak) id<CreditCardSuggestionBottomSheetConsumer> consumer;
 
+// The mediator's delegate.
+@property(nonatomic, weak) id<CreditCardSuggestionBottomSheetMediatorDelegate>
+    delegate;
+
 // Whether the bottom sheet has any credit cards to display.
 @property(nonatomic, readonly) BOOL hasCreditCards;
 
diff --git a/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mm b/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mm
index 7ac20e50..c443cd6 100644
--- a/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mm
+++ b/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mm
@@ -24,7 +24,6 @@
 #import "components/autofill/ios/form_util/form_activity_params.h"
 #import "components/strings/grit/components_strings.h"
 #import "ios/chrome/browser/autofill/model/autofill_tab_helper.h"
-#import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_java_script_feature.h"
 #import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h"
 #import "ios/chrome/browser/autofill/model/credit_card/credit_card_data.h"
 #import "ios/chrome/browser/autofill/model/features.h"
@@ -36,7 +35,7 @@
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer_bridge.h"
 #import "ios/chrome/grit/ios_strings.h"
-#import "ios/web/public/js_messaging/web_frames_manager.h"
+#import "ios/web/public/navigation/navigation_context.h"
 #import "ios/web/public/web_state.h"
 #import "ios/web/public/web_state_observer_bridge.h"
 #import "ui/base/l10n/l10n_util.h"
@@ -57,20 +56,24 @@
   return base::FeatureList::IsEnabled(kAutofillPaymentsSheetV3Ios);
 }
 
+// Status of the form fill context for the payments bottom sheet.
+enum class FillContextStatus {
+  // Context is valid; suggestions may be accepted and filled.
+  kValid,
+  // Context was invalidated by cross-document navigation, tab detachment, or
+  // WebState destruction. Selecting a card must NOT refocus the field.
+  kInvalidatedByWebStateChange,
+  // Context was invalidated because retrieved suggestions contained no credit
+  // cards. Selecting a card dismisses the sheet and refocuses the field.
+  kInvalidatedNoSuggestions,
+};
+
 }  // namespace
 
 @interface CreditCardSuggestionBottomSheetMediator () <
     CRWWebStateObserver,
     PersonalDataManagerObserver,
     WebStateListObserving>
-
-// YES if the context is still valid for filling. Determined based the outcome
-// of a best effort to retrieve suggestions before the user accepts a
-// suggestion. The context is deemed valid by default to keep the status quo.
-// Context is invalidated if the suggestions are retrieved in time and
-// contradict the context (i.e. suggestions aren't credit card suggestions).
-@property(nonatomic, assign) BOOL fillContextIsValid;
-
 @end
 
 @implementation CreditCardSuggestionBottomSheetMediator {
@@ -104,6 +107,9 @@
   // to when the presentation animation is done. Countdowns start once this
   // timestamp is set with a value.
   std::optional<base::TimeTicks> _viewDidAppearTimestamp;
+
+  // Status of the fill context.
+  FillContextStatus _fillContextStatus;
 }
 
 #pragma mark - Properties
@@ -121,7 +127,7 @@
     _hasCreditCards = NO;
     _webStateList = webStateList;
     // Context deemed valid by default; status quo.
-    _fillContextIsValid = YES;
+    _fillContextStatus = FillContextStatus::kValid;
     if (personalDataManager) {
       _personalDataManager = personalDataManager;
       _personalDataManagerObserver =
@@ -152,6 +158,11 @@
 }
 
 - (void)disconnect {
+  // Clearing the consumer goes through `-setConsumer:`, which is a no-op for a
+  // nil consumer; it only drops the reference.
+  self.consumer = nil;
+  self.delegate = nil;
+
   _personalDataManagerObserver = nullptr;
   _personalDataManager = nullptr;
 
@@ -172,10 +183,6 @@
   return card ? std::make_optional(*card) : std::nullopt;
 }
 
-- (BOOL)hasCreditCards {
-  return _hasCreditCards;
-}
-
 - (void)logExitReason:(PaymentsSuggestionBottomSheetExitReason)exitReason {
   base::UmaHistogramEnumeration("IOS.PaymentsBottomSheet.ExitReason",
                                 exitReason);
@@ -235,6 +242,15 @@
 
 - (void)didSelectCreditCard:(CreditCardData*)creditCardData
                     atIndex:(NSInteger)index {
+  // Navigation or WebState change has occurred; the originating frame is
+  // detached or destroyed. Suppress refocusing to prevent improperly focusing
+  // elements or summoning the keyboard on the new page. No metric is logged
+  // as this is not a suggestion provider error.
+  if (_fillContextStatus == FillContextStatus::kInvalidatedByWebStateChange) {
+    [self disableBottomSheetAndRefocus:NO];
+    return;
+  }
+
   web::WebState* activeWebState = [self getActiveWebState];
   if (!activeWebState) {
     return;
@@ -252,7 +268,7 @@
   // set incorrectly (for example if local predictions and server
   // predictions are different), simply exit and open the keyboard.
   if (IsStateless()) {
-    if (!self.fillContextIsValid) {
+    if (_fillContextStatus != FillContextStatus::kValid) {
       [self disableBottomSheetAndRefocus:YES];
       [self logExitReason:kBadProvider];
       return;
@@ -319,6 +335,15 @@
 }
 
 - (void)disableBottomSheetAndRefocus:(BOOL)shouldRefocus {
+  // Suppress refocusing when the context was invalidated by navigation or
+  // WebState change. Callers (such as `viewDidDisappear:`) default to
+  // requesting refocus on exit, but if the WebState has navigated to a new
+  // document or changed, refocusing would improperly summon the keyboard or
+  // focus an element on the new page.
+  if (_fillContextStatus == FillContextStatus::kInvalidatedByWebStateChange) {
+    shouldRefocus = NO;
+  }
+
   bool useV2 = base::FeatureList::IsEnabled(kAutofillPaymentsSheetV2Ios);
   if (useV2) {
     // Do not remove the listeners for the bottom sheet (aka disable) in V2
@@ -375,14 +400,39 @@
 
 - (void)webStateListDestroyed:(WebStateList*)webStateList {
   DCHECK_EQ(webStateList, _webStateList);
-  // `disconnect` cleans up all references to `_webStateList` and objects that
-  // depend on it.
-  [self disconnect];
+  // Dismiss and clean up UI before disconnecting from WebStateList.
   [self onWebStateChange];
+  [self disconnect];
 }
 
 #pragma mark - CRWWebStateObserver
 
+// Invalidation happens as soon as the navigation starts, before the new
+// document can be committed, so that a suggestion retrieved for the previous
+// document can never be filled into the next one. The trade-off is that a
+// cross-document navigation that ends up being aborted still tears the sheet
+// down, which is the safe outcome.
+- (void)webState:(web::WebState*)webState
+    didStartNavigation:(web::NavigationContext*)navigationContext {
+  CHECK(navigationContext);
+  if (navigationContext->IsSameDocument()) {
+    return;
+  }
+
+  [self onWebStateChange];
+}
+
+- (void)webState:(web::WebState*)webState
+    didFinishNavigation:(web::NavigationContext*)navigationContext {
+  CHECK(navigationContext);
+  if (navigationContext->IsSameDocument() ||
+      !navigationContext->HasCommitted()) {
+    return;
+  }
+
+  [self onWebStateChange];
+}
+
 - (void)webStateDestroyed:(web::WebState*)webState {
   [self onWebStateChange];
 }
@@ -393,8 +443,25 @@
 
 #pragma mark - Private
 
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper_unittest.mm b/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper_unittest.mm
index 1f038f2..69a5b1c 100644
--- a/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper_unittest.mm
+++ b/ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper_unittest.mm
@@ -18,6 +18,7 @@
 #import "components/autofill/ios/browser/autofill_agent.h"
 #import "components/autofill/ios/browser/autofill_driver_ios.h"
 #import "components/autofill/ios/browser/autofill_driver_ios_factory.h"
+#import "components/autofill/ios/browser/form_suggestion.h"
 #import "components/autofill/ios/browser/test_autofill_client_ios.h"
 #import "components/autofill/ios/common/features.h"
 #import "components/infobars/core/infobar.h"
@@ -25,6 +26,10 @@
 #import "components/password_manager/core/browser/features/password_features.h"
 #import "components/personal_context/first_run/personal_context_first_run_service.h"
 #import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_java_script_feature.h"
+#import "ios/chrome/browser/autofill/model/features.h"
+#import "ios/chrome/browser/autofill/model/form_input_suggestions_provider.h"
+#import "ios/chrome/browser/autofill/model/form_suggestion_controller.h"
+#import "ios/chrome/browser/autofill/model/form_suggestion_tab_helper.h"
 #import "ios/chrome/browser/autofill/model/ios_autofill_entity_data_manager_factory.h"
 #import "ios/chrome/browser/autofill/model/personal_data_manager_factory.h"
 #import "ios/chrome/browser/autofill/model/strike_database_factory.h"
@@ -38,7 +43,9 @@
 #import "ios/chrome/browser/web/model/chrome_web_client.h"
 #import "ios/chrome/browser/webdata_services/model/web_data_service_factory.h"
 #import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
+#import "ios/testing/scoped_block_swizzler.h"
 #import "ios/web/public/js_messaging/script_message.h"
+#import "ios/web/public/test/fakes/fake_navigation_context.h"
 #import "ios/web/public/test/js_test_util.h"
 #import "ios/web/public/test/scoped_testing_web_client.h"
 #import "ios/web/public/test/web_state_test_util.h"
@@ -106,15 +113,25 @@
 
 }  // namespace
 
-// Fake implementation of AutofillCommands to capture ambient notice triggers.
+// Fake implementation of `AutofillCommands` to capture triggers in tests.
 @interface FakeAutofillCommandsForBottomSheet : NSObject
 @property(nonatomic, assign) BOOL showAmbientAutofillNoticeCalled;
+@property(nonatomic, assign) int dismissEditAddressBottomSheetCallCount;
+@property(nonatomic, assign) BOOL showPaymentsBottomSheetCalled;
 @end
 
 @implementation FakeAutofillCommandsForBottomSheet
 - (void)showAmbientAutofillNotice:(const autofill::FormActivityParams&)params {
   _showAmbientAutofillNoticeCalled = YES;
 }
+
+- (void)dismissEditAddressBottomSheet {
+  _dismissEditAddressBottomSheetCallCount++;
+}
+
+- (void)showPaymentsBottomSheet:(const autofill::FormActivityParams&)params {
+  _showPaymentsBottomSheetCalled = YES;
+}
 @end
 
 // Test fixture to test AutofillBottomSheetTabHelper class.
@@ -614,3 +631,202 @@
   // Verify that showAmbientAutofillNotice: is triggered.
   EXPECT_TRUE(commands_handler.showAmbientAutofillNoticeCalled);
 }
+
+// Tests that setting the autofill bottom sheet handler to nil triggers
+// dismissal of the edit address bottom sheet.
+TEST_F(AutofillBottomSheetTabHelperTest,
+       DismissesEditAddressBottomSheetWhenHandlerResetToNil) {
+  FakeAutofillCommandsForBottomSheet* commands_handler =
+      [[FakeAutofillCommandsForBottomSheet alloc] init];
+  helper_->SetAutofillBottomSheetHandler(
+      (id<AutofillCommands>)commands_handler);
+  EXPECT_EQ(commands_handler.dismissEditAddressBottomSheetCallCount, 0);
+
+  // Reset handler to nil.
+  helper_->SetAutofillBottomSheetHandler(nil);
+  EXPECT_EQ(commands_handler.dismissEditAddressBottomSheetCallCount, 1);
+}
+
+// Test that DidFinishNavigation clears registered renderer IDs on
+// cross-document navigation but retains them on same-document navigation.
+TEST_F(AutofillBottomSheetTabHelperTest,
+       DidFinishNavigationClearsRegisteredRendererIds) {
+  web::WebFrame* frame = nullptr;
+  autofill::AutofillManager* manager = nullptr;
+  autofill::FormGlobalId form_id;
+  SetupAmbientNoticeForm(autofill::FieldRendererId(0), frame, manager, form_id);
+
+  // Register the listeners.
+  helper_->UpdateListenersForAmbientAutofillForm(*manager, form_id,
+                                                 /*only_new=*/false);
+
+  // Set up the fake command handler.
+  FakeAutofillCommandsForBottomSheet* commands_handler =
+      [[FakeAutofillCommandsForBottomSheet alloc] init];
+  helper_->SetAutofillBottomSheetHandler(
+      (id<AutofillCommands>)commands_handler);
+
+  // Simulate same-document navigation: registered IDs should be preserved.
+  web::FakeNavigationContext same_doc_context;
+  same_doc_context.SetIsSameDocument(true);
+  same_doc_context.SetHasCommitted(true);
+  helper_->DidFinishNavigation(web_state_.get(), &same_doc_context);
+
+  web::ScriptMessage form_message =
+      ScriptMessageForForm(ValidFormMessageBody(frame->GetFrameId()));
+  helper_->OnFormMessageReceived(form_message);
+  EXPECT_TRUE(commands_handler.showAmbientAutofillNoticeCalled);
+
+  // Reset handler state.
+  commands_handler.showAmbientAutofillNoticeCalled = NO;
+
+  // Simulate uncommitted cross-document navigation: registered IDs should be
+  // preserved.
+  web::FakeNavigationContext uncommitted_context;
+  uncommitted_context.SetIsSameDocument(false);
+  uncommitted_context.SetHasCommitted(false);
+  helper_->DidFinishNavigation(web_state_.get(), &uncommitted_context);
+
+  helper_->OnFormMessageReceived(form_message);
+  EXPECT_TRUE(commands_handler.showAmbientAutofillNoticeCalled);
+
+  // Reset handler state.
+  commands_handler.showAmbientAutofillNoticeCalled = NO;
+
+  // Simulate committed cross-document navigation: registered IDs should be
+  // cleared.
+  web::FakeNavigationContext cross_doc_context;
+  cross_doc_context.SetIsSameDocument(false);
+  cross_doc_context.SetHasCommitted(true);
+  helper_->DidFinishNavigation(web_state_.get(), &cross_doc_context);
+
+  helper_->OnFormMessageReceived(form_message);
+  EXPECT_FALSE(commands_handler.showAmbientAutofillNoticeCalled);
+}
+
+// Test that DidStartNavigation invalidates pending suggestion retrieval
+// callbacks, preventing bottom sheet presentation on cross-document
+// navigation, while same-document navigation preserves them.
+TEST_F(AutofillBottomSheetTabHelperTest,
+       DidStartNavigationCancelsPendingCallbacks) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      {kStatelessFormSuggestionController, kAutofillPaymentsSheetV3Ios}, {});
+
+  FormSuggestionTabHelper::CreateForWebState(web_state_.get(), @[]);
+
+  __block FormSuggestionsReadyCompletion captured_completion = nil;
+  id swizzle_block =
+      ^(id self, const autofill::FormActivityParams& params,
+        web::WebState* webState, FormSuggestionsReadyCompletion completion) {
+        captured_completion = [completion copy];
+      };
+  ScopedBlockSwizzler swizzler(
+      [FormSuggestionController class],
+      @selector(retrieveSuggestionsForForm:webState:accessoryViewUpdateBlock:),
+      swizzle_block);
+
+  FakeAutofillCommandsForBottomSheet* commands_handler =
+      [[FakeAutofillCommandsForBottomSheet alloc] init];
+  helper_->SetAutofillBottomSheetHandler(
+      (id<AutofillCommands>)commands_handler);
+
+  autofill::FormActivityParams params;
+  params.form_name = "test_form";
+  params.field_identifier = "test_field";
+  params.frame_id = "test_frame";
+
+  FormSuggestion* test_suggestion = [FormSuggestion
+      suggestionWithValue:@"Visa **** 1234"
+       displayDescription:@"Card description"
+                     icon:nil
+                     type:autofill::SuggestionType::kCreditCardEntry
+                  payload:autofill::Suggestion::Payload()
+           requiresReauth:NO];
+
+  // Request bottom sheet for payments and verify completion is captured.
+  helper_->MaybeShowPaymentsBottomSheet(params);
+  ASSERT_TRUE(captured_completion != nil);
+
+  // Simulate cross-document navigation start: cancels weak pointers.
+  web::FakeNavigationContext cross_doc_context;
+  cross_doc_context.SetIsSameDocument(false);
+  helper_->DidStartNavigation(web_state_.get(), &cross_doc_context);
+
+  // Execute completion block: should be ignored due to weak pointer
+  // invalidation.
+  captured_completion(@[ test_suggestion ], nil);
+  EXPECT_FALSE(commands_handler.showPaymentsBottomSheetCalled);
+
+  // Request bottom sheet again.
+  captured_completion = nil;
+  helper_->MaybeShowPaymentsBottomSheet(params);
+  ASSERT_TRUE(captured_completion != nil);
+
+  // Simulate same-document navigation start: preserves callbacks.
+  web::FakeNavigationContext same_doc_context;
+  same_doc_context.SetIsSameDocument(true);
+  helper_->DidStartNavigation(web_state_.get(), &same_doc_context);
+
+  // Execute completion block: should present the payments bottom sheet.
+  captured_completion(@[ test_suggestion ], nil);
+  EXPECT_TRUE(commands_handler.showPaymentsBottomSheetCalled);
+}
+
+// Test that RenderProcessGone cancels pending suggestion retrieval callbacks
+// and clears registered renderer IDs.
+TEST_F(AutofillBottomSheetTabHelperTest, RenderProcessGoneClearsState) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      {kStatelessFormSuggestionController, kAutofillPaymentsSheetV3Ios}, {});
+
+  FormSuggestionTabHelper::CreateForWebState(web_state_.get(), @[]);
+
+  web::WebFrame* frame = nullptr;
+  autofill::AutofillManager* manager = nullptr;
+  autofill::FormGlobalId form_id;
+  SetupAmbientNoticeForm(autofill::FieldRendererId(0), frame, manager, form_id);
+  helper_->UpdateListenersForAmbientAutofillForm(*manager, form_id,
+                                                 /*only_new=*/false);
+
+  __block FormSuggestionsReadyCompletion captured_completion = nil;
+  id swizzle_block =
+      ^(id self, const autofill::FormActivityParams& params,
+        web::WebState* webState, FormSuggestionsReadyCompletion completion) {
+        captured_completion = [completion copy];
+      };
+  ScopedBlockSwizzler swizzler(
+      [FormSuggestionController class],
+      @selector(retrieveSuggestionsForForm:webState:accessoryViewUpdateBlock:),
+      swizzle_block);
+
+  FakeAutofillCommandsForBottomSheet* commands_handler =
+      [[FakeAutofillCommandsForBottomSheet alloc] init];
+  helper_->SetAutofillBottomSheetHandler(
+      (id<AutofillCommands>)commands_handler);
+
+  autofill::FormActivityParams params;
+  params.form_name = "test_form";
+  params.field_identifier = "test_field";
+  params.frame_id = "test_frame";
+
+  helper_->MaybeShowPaymentsBottomSheet(params);
+  ASSERT_TRUE(captured_completion != nil);
+
+  helper_->RenderProcessGone(web_state_.get());
+
+  FormSuggestion* test_suggestion = [FormSuggestion
+      suggestionWithValue:@"Visa **** 1234"
+       displayDescription:@"Card description"
+                     icon:nil
+                     type:autofill::SuggestionType::kCreditCardEntry
+                  payload:autofill::Suggestion::Payload()
+           requiresReauth:NO];
+  captured_completion(@[ test_suggestion ], nil);
+  EXPECT_FALSE(commands_handler.showPaymentsBottomSheetCalled);
+
+  web::ScriptMessage form_message =
+      ScriptMessageForForm(ValidFormMessageBody(frame->GetFrameId()));
+  helper_->OnFormMessageReceived(form_message);
+  EXPECT_FALSE(commands_handler.showAmbientAutofillNoticeCalled);
+}
diff --git a/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator_unittest.mm b/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator_unittest.mm
index 10feb655..4c58fd35ce 100644
--- a/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator_unittest.mm
+++ b/ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator_unittest.mm
@@ -21,12 +21,25 @@
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
 #import "ios/testing/scoped_block_swizzler.h"
+#import "ios/web/public/test/fakes/fake_navigation_context.h"
 #import "ios/web/public/test/fakes/fake_web_state.h"
 #import "ios/web/public/test/web_task_environment.h"
 #import "testing/platform_test.h"
 #import "third_party/ocmock/OCMock/OCMock.h"
 #import "third_party/ocmock/gtest_support.h"
 
+@class CreditCardData;
+@class FormSuggestion;
+
+@interface CreditCardSuggestionBottomSheetMediator (Testing)
+- (void)onSuggestionsRetrieved:(NSArray<FormSuggestion*>*)suggestions;
+- (void)fillCreditCard:(CreditCardData*)creditCardData
+               atIndex:(NSInteger)index
+         crossProvider:(id<FormInputSuggestionsProvider>)crossProvider;
+- (void)disableBottomSheetAndRefocus:(BOOL)shouldRefocus;
+- (void)refocus;
+@end
+
 namespace {
 
 const char kTestNumber[] = "4234567890123456";  // Visa
@@ -44,12 +57,15 @@
 
     consumer_ = OCMStrictProtocolMock(
         @protocol(CreditCardSuggestionBottomSheetConsumer));
+    delegate_ = OCMStrictProtocolMock(
+        @protocol(CreditCardSuggestionBottomSheetMediatorDelegate));
 
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Chrome iOS rebinds a stale Payments Bottom Sheet card choice to the post-navigation document


Report description

Chrome iOS rebinds a stale Payments Bottom Sheet card choice to the post-navigation document


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

Overview

Chrome for iOS retains document A’s card selection and field parameters in the Payments Bottom Sheet but does not dismiss the sheet after a same-tab, cross-document navigation. Document B can focus a payment field with script; native code upgrades that event to a user gesture and refreshes the shared Autofill delegate. Accepting A’s still-visible sheet then fills B.

Affected files

File Relevant behavior
ios/chrome/browser/autofill/model/bottom_sheet/bottom_sheet.ts Forwards the focus event’s trust bit.
ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm Forces has_user_gesture and does not dismiss the visible sheet on navigation.
ios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mm Retains A’s parameters but resolves the active WebState/provider at acceptance.
components/autofill/ios/browser/autofill_agent.mm Accepts through the current mutable delegate without document validation.

Affected versions

Source snapshot Exact Chromium version Revision Result
Stable audit baseline 151.0.7922.108 4744b886309d987d292e43232776d2206cccb13d Affected logic present
Audited HEAD 154.0.8012.0 34a482b3cd98d6d599f43091babae366fb9c3d8e Affected logic present
Newer origin/main snapshot 154.0.8018.0 66e4be60020fc281e94abfdcb37b86389283c03f Affected logic present

The source chain is present in Chrome 151 source, Chromium revision 34a482b3cd98d6d599f43091babae366fb9c3d8e, and inspected newer revision 66e4be60020fc281e94abfdcb37b86389283c03f. A real-world iPhone PoC is provided.

Regression / FoundIn

The vulnerable ordering is present in all three inspected revisions. No unsupported earliest milestone or first-bad commit is claimed. The source validator confirms the relevant Payments-sheet features are enabled by default in the inspected baselines.

Platform matrix

Platform Result
Chrome for iOS source Affected in all three inspected revisions
Real-world iPhone PoC Supplied with controlled A/B pages and test card data
Released Chrome for iOS Real-world two-origin PoC supplied
Desktop Chromium Uses a different Autofill implementation

Vulnerability details

The sheet mediator stores A’s FormActivityParams but does not observe cross-document navigation. B’s scripted focus is accepted because native code sets has_user_gesture = true. When the user taps Continue, the mediator queries the active WebState and current provider, while still forwarding A’s stored parameters. The Autofill agent then accepts through the mutable current delegate without proving that the sheet, form, delegate, and committed document are the same objects originally authorized.

Safe reproduction

  1. Serve a.html and b.html from two controlled origins and use a fresh Chrome for iOS test profile containing only a test payment card.
  2. Open A, focus its card-number field, and allow the Payments Bottom Sheet to appear.
  3. While the sheet remains visible, navigate the same tab to B. B automatically focuses its controlled payment field.
  4. Tap Continue on the stale sheet.
  5. Confirm B’s page observes the test card values. As controls, repeat after dismissing the sheet and repeat with B lacking a payment field; neither control should receive a fill.

MANUAL_POC.md contains the complete device sequence.

Actual results

The three-revision validator passed and confirmed that the Payments Bottom Sheet can survive same-tab document replacement while its provider resolves the current focused frame at acceptance time. The A/B pages preserve one stale sheet sequence and expose the canary fill result directly in B.

Expected behavior

Chrome must dismiss or invalidate the Payments Bottom Sheet on a different- document navigation. Acceptance must validate a document-scoped frame/form token and must never upgrade an untrusted focus event into a user gesture.

PoC Files

Category File Purpose SHA-256
Device instructions MANUAL_POC.md Real-world Chrome for iOS reproduction and controls 9f7c98c7011900f8f750ea45f5bcb1393322f30f27d109f633aedb1f1c53e866
Source page a.html Opens the sheet for the originally authorized document 4386e20e01ab71d049ee28a5085ac1823f52f84aedaa720a34d3f2dae29821ba
Destination page b.html Focuses and records the post-navigation card fill 651fdbe964669c68ac9bb9383073229496d2399ba4acb3ef3d41947e31335c89

Impact analysis

Impact

  • A cross-origin destination can receive a saved card’s PAN, name, and expiry selected for the previous document.
  • The destination can submit the newly filled test-equivalent values through its own origin, adding an integrity consequence.

The cause

What version of Chrome have you found the security issue in?

151.0.7922.108 + stable

No, it is not related to a crash.

Choose the type of vulnerability

Sensitive data exposure

How would you like to be publicly acknowledged for your report?

Anonymous

View on issue tracker