CVE-2026-106203
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mm |
modified | |
WebStateListios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.h |
modified | |
ifios/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.mmios/chrome/browser/autofill/payments/coordinator/payments_suggestion_bottom_sheet_coordinator.mmtools/metrics/histograms/metadata/ios/enums.xmlios/chrome/browser/autofill/payments/test/BUILD.gnios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.mmios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.hios/chrome/browser/autofill/payments/coordinator/BUILD.gnios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator.hios/chrome/browser/autofill/payments/coordinator/credit_card_suggestion_bottom_sheet_mediator_unittest.mmios/chrome/browser/autofill/payments/coordinator/payments_suggestion_bottom_sheet_exit_reason.hios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.mmios/chrome/browser/autofill/payments/test/payments_suggestion_bottom_sheet_egtest.mmios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper_unittest.mmios/chrome/browser/autofill/model/bottom_sheet/BUILD.gn
Patch
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)
Regression Test / PoC
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)
Original Bug Report
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
- Serve
a.htmlandb.htmlfrom two controlled origins and use a fresh Chrome for iOS test profile containing only a test payment card. - Open A, focus its card-number field, and allow the Payments Bottom Sheet to appear.
- While the sheet remains visible, navigate the same tab to B. B automatically focuses its controlled payment field.
- Tap Continue on the stale sheet.
- 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
Is the security issue related to a crash?
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