CVE-2026-106387
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mm |
modified | |
ManualFillCardCellTestios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm |
modified | |
TEST_Fios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm |
modified |
Files Changed
ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mmios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mmios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell+Testing.hios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mmios/chrome/browser/autofill/manual_fill/test/BUILD.gnios/chrome/browser/autofill/manual_fill/ui/BUILD.gn
Patch
From a557999d68047e2a54f2f20560dae9ab90d89b48 Mon Sep 17 00:00:00 2001
From: Alexis Hétu <sugoi@chromium.org>
Date: Thu, 10 Sep 2026 13:53:30 -0700
Subject: [PATCH] [iOS] Enforce HTTPS injection checks on manual fill card cell actions
Credit card manual fill actions must only inject into secure (HTTPS)
contexts. While tapping the card number was gated by an HTTPS check,
tapping the "Autofill Form" button, the CVC button for cards that
cannot be filled directly (e.g. masked server cards), and the
expiration date chips for virtual cards initiated full card unmasking
or form filling without first checking if the context is secure.
This CL adds `canUserInjectInPasswordField:NO requiresHTTPS:YES` guards
to `onAutofillFormButtonTapped`, `userDidTapCVC:`, and virtual card
expiration date taps before recording metrics or requesting full card
unmasking, and adds unit tests covering these cases.
Bug: 518096516
Change-Id: Ib76fdf460802dd7bbd3b87761bb08695ebca62f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8371483
Reviewed-by: Vincent Boisselle <vincb@google.com>
Commit-Queue: Alexis Hétu <sugoi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1695639}
---
diff --git a/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell+Testing.h b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell+Testing.h
index 86e2c0321..2111686 100644
--- a/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell+Testing.h
+++ b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell+Testing.h
@@ -7,7 +7,10 @@
#import "ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.h"
-// Testing category exposing private properties of ManualFillCardItem for tests.
+@class UIButton;
+
+// Testing category exposing private properties of `ManualFillCardItem` for
+// tests.
@interface ManualFillCardItem (Testing)
// The 0-based index at which the payment method is in the list of payment
@@ -21,4 +24,22 @@
@end
+// Testing category exposing private properties and subviews of
+// `ManualFillCardCell` for tests.
+@interface ManualFillCardCell (Testing)
+
+// The button to autofill the current form with the card's data.
+@property(nonatomic, strong, readonly) UIButton* autofillFormButton;
+
+// The button to autofill or request the card's CVC.
+@property(nonatomic, readonly) UIButton* cvcButton;
+
+// The button to autofill or request the card's expiration month.
+@property(nonatomic, readonly) UIButton* expirationMonthButton;
+
+// The button to autofill or request the card's expiration year.
+@property(nonatomic, readonly) UIButton* expirationYearButton;
+
+@end
+
#endif // IOS_CHROME_BROWSER_AUTOFILL_MANUAL_FILL_UI_MANUAL_FILL_CARD_CELL_TESTING_H_
diff --git a/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mm b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mm
index fa8e9645..e250df4e 100644
--- a/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mm
+++ b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mm
@@ -607,6 +607,10 @@
base::RecordAction(base::UserMetricsAction(
[self createMetricsAction:@"SelectExpirationMonth"]));
if (self.card.recordType == kVirtualCard) {
+ if (![self.contentInjector canUserInjectInPasswordField:NO
+ requiresHTTPS:YES]) {
+ return;
+ }
[self.navigationDelegate
requestFullCreditCard:self.card
fieldType:manual_fill::PaymentFieldType::kExpirationMonth];
@@ -624,6 +628,10 @@
base::RecordAction(base::UserMetricsAction(
[self createMetricsAction:@"SelectExpirationYear"]));
if (self.card.recordType == kVirtualCard) {
+ if (![self.contentInjector canUserInjectInPasswordField:NO
+ requiresHTTPS:YES]) {
+ return;
+ }
[self.navigationDelegate
requestFullCreditCard:self.card
fieldType:manual_fill::PaymentFieldType::kExpirationYear];
@@ -638,16 +646,17 @@
}
- (void)userDidTapCVC:(UIButton*)sender {
+ if (![self.contentInjector canUserInjectInPasswordField:NO
+ requiresHTTPS:YES]) {
+ return;
+ }
+
base::RecordAction(
base::UserMetricsAction([self createMetricsAction:@"SelectCvc"]));
// For cards that can be filled directly (e.g. local cards or unmasked virtual
// cards), insert the stored CVC.
if (self.card.canFillDirectly) {
- if (![self.contentInjector canUserInjectInPasswordField:NO
- requiresHTTPS:YES]) {
- return;
- }
[self.contentInjector
userDidPickContent:self.card.CVC
passwordField:NO
@@ -664,6 +673,11 @@
// Called when the "Autofill Form" button is tapped. Fills the current form with
// the card's data.
- (void)onAutofillFormButtonTapped {
+ if (![self.contentInjector canUserInjectInPasswordField:NO
+ requiresHTTPS:YES]) {
+ return;
+ }
+
base::UmaHistogramSparse(
"Autofill.UserAcceptedSuggestionAtIndex.CreditCard.ManualFallback",
_cellIndex);
@@ -851,4 +865,18 @@
return imageView;
}
+#pragma mark - Testing
+
+- (UIButton*)cvcButton {
+ return self.CVCLabeledChip.singleButton;
+}
+
+- (UIButton*)expirationMonthButton {
+ return self.expirationDateLabeledChip.expirationMonthButton;
+}
+
+- (UIButton*)expirationYearButton {
+ return self.expirationDateLabeledChip.expirationYearButton;
+}
+
@end
diff --git a/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm b/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm
index f37e51f..b60f36e 100644
--- a/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm
+++ b/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm
@@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#import <optional>
+
#import "base/ios/ios_util.h"
+#import "base/path_service.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/autofill/core/browser/test_utils/autofill_test_util.h"
@@ -23,6 +26,7 @@
#import "ios/testing/earl_grey/app_launch_configuration.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/public/test/element_selector.h"
+#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"
@@ -281,7 +285,9 @@
} // namespace
// Integration Tests for Manual Fallback credit cards View Controller.
-@interface CreditCardViewControllerTestCase : ChromeTestCase
+@interface CreditCardViewControllerTestCase : ChromeTestCase {
+ std::optional<net::test_server::EmbeddedTestServer> _HTTPSServer;
+}
@end
@implementation CreditCardViewControllerTestCase
@@ -295,6 +301,7 @@
- (void)setUp {
[super setUp];
+ _HTTPSServer.reset();
GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
const GURL URL = self.testServer->GetURL(kFormHTMLFile);
[self loadURL];
@@ -318,6 +325,7 @@
[MetricsAppInterface stopOverridingMetricsAndCrashReportingForTesting];
chrome_test_util::GREYAssertErrorNil(
[MetricsAppInterface releaseHistogramTester]);
+ _HTTPSServer.reset();
[super tearDownHelper];
}
@@ -1005,6 +1013,22 @@
[ReauthenticationAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
+ [self startHTTPSServer];
+ const GURL URL = _HTTPSServer->GetURL(kFormHTMLFile);
+ [ChromeEarlGrey loadURL:URL];
+
+ // Check if the SSL warning page is displayed by verifying if the
+ // "details-button" element exists on the page.
+ base::Value result = [ChromeEarlGrey
+ evaluateJavaScript:@"document.getElementById('details-button') !== null"];
+ if (result.is_bool() && result.GetBool()) {
+ [ChromeEarlGrey tapWebStateElementWithID:@"details-button"];
+ [ChromeEarlGrey tapWebStateElementWithID:@"proceed-link"];
+ }
+
+ [ChromeEarlGrey waitForWebStateContainingText:"Autofill Test"];
+ [AutofillAppInterface considerCreditCardFormSecureForTesting];
+
// Save a card.
[AutofillAppInterface saveLocalCreditCard];
@@ -1077,6 +1101,16 @@
[ChromeEarlGrey waitForWebStateContainingText:"Autofill Test"];
}
+// Starts the dedicated HTTPS test server.
+- (void)startHTTPSServer {
+ _HTTPSServer.emplace(net::test_server::EmbeddedTestServer::TYPE_HTTPS);
+ _HTTPSServer->ServeFilesFromDirectory(
+ base::PathService::CheckedGet(base::DIR_ASSETS)
+ .AppendASCII("ios/testing/data/http_server_files/"));
+ net::test_server::RegisterDefaultHandlers(&_HTTPSServer.value());
+ GREYAssertTrue(_HTTPSServer->Start(), @"HTTPS Test server failed to start.");
+}
+
- (void)verifyCreditCardButtonWithTitle:(NSString*)title
doesInjectValue:(NSString*)result {
[AutofillAppInterface saveLocalCreditCard];
diff --git a/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm
new file mode 100644
index 0000000..218e571
--- /dev/null
+++ b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm
@@ -0,0 +1,330 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.h"
+
+#import "ios/chrome/browser/autofill/manual_fill/model/manual_fill_credit_card.h"
+#import "ios/chrome/browser/autofill/manual_fill/public/manual_fill_content_injector.h"
+#import "ios/chrome/browser/autofill/manual_fill/ui/card_list_delegate.h"
+#import "ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell+Testing.h"
+#import "testing/gtest_mac.h"
+#import "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+#import "third_party/ocmock/gtest_support.h"
+
+namespace {
+
+// Returns a card sufficient to populate a `ManualFillCardCell`.
+ManualFillCreditCard* TestLocalCard() {
+ return [[ManualFillCreditCard alloc]
+ initWithGUID:@"00000000-0000-0000-0000-000000000001"
+ network:@"Visa"
+ icon:nil
+ bankName:nil
+ cardHolder:@"Test User"
+ number:@"4111111111111111"
+ obfuscatedNumber:@"••••1111"
+ networkAndLastFourDigits:nil
+ expirationYear:@"2100"
+ expirationMonth:@"01"
+ CVC:nil
+ recordType:autofill::CreditCard::RecordType::
+ kLocalCard
+ cardInfoRetrievalEnrollmentState:
+ autofill::CreditCard::CardInfoRetrievalEnrollmentState::
+ kRetrievalUnenrolledAndNotEligible
+ canFillDirectly:YES];
+}
+
+// Returns a masked server card that cannot be filled directly.
+ManualFillCreditCard* TestServerCard() {
+ return [[ManualFillCreditCard alloc]
+ initWithGUID:@"00000000-0000-0000-0000-000000000002"
+ network:@"Visa"
+ icon:nil
+ bankName:nil
+ cardHolder:@"Test User"
+ number:@"4111111111111111"
+ obfuscatedNumber:@"••••1111"
+ networkAndLastFourDigits:nil
+ expirationYear:@"2100"
+ expirationMonth:@"01"
+ CVC:@"•••"
+ recordType:autofill::CreditCard::RecordType::
+ kMaskedServerCard
+ cardInfoRetrievalEnrollmentState:
+ autofill::CreditCard::CardInfoRetrievalEnrollmentState::
+ kRetrievalUnenrolledAndNotEligible
+ canFillDirectly:NO];
+}
+
+// Returns a virtual card.
+ManualFillCreditCard* TestVirtualCard() {
+ return [[ManualFillCreditCard alloc]
+ initWithGUID:@"00000000-0000-0000-0000-000000000003"
+ network:@"Visa"
+ icon:nil
+ bankName:nil
+ cardHolder:@"Test User"
+ number:@"4111111111111111"
+ obfuscatedNumber:@"••••1111"
+ networkAndLastFourDigits:nil
+ expirationYear:@"2100"
+ expirationMonth:@"01"
+ CVC:nil
+ recordType:autofill::CreditCard::RecordType::
+ kVirtualCard
+ cardInfoRetrievalEnrollmentState:
+ autofill::CreditCard::CardInfoRetrievalEnrollmentState::
+ kRetrievalUnenrolledAndNotEligible
+ canFillDirectly:NO];
+}
+
+// Returns the "Autofill Form" button of `cell`.
+UIButton* AutofillFormButton(ManualFillCardCell* cell) {
+ return cell.autofillFormButton;
+}
+
+// Returns the CVC button of `cell`.
+UIButton* CvcButton(ManualFillCardCell* cell) {
+ return cell.cvcButton;
+}
+
+// Returns the expiration month button of `cell`.
+UIButton* ExpirationMonthButton(ManualFillCardCell* cell) {
+ return cell.expirationMonthButton;
+}
+
+// Returns the expiration year button of `cell`.
+UIButton* ExpirationYearButton(ManualFillCardCell* cell) {
+ return cell.expirationYearButton;
+}
+
+} // namespace
+
+class ManualFillCardCellTest : public PlatformTest {
+ protected:
+ void SetUp() override {
+ PlatformTest::SetUp();
+ content_injector_ = OCMProtocolMock(@protocol(ManualFillContentInjector));
+ navigation_delegate_ = OCMProtocolMock(@protocol(CardListDelegate));
+ card_ = TestLocalCard();
+ cell_ = [[ManualFillCardCell alloc] init];
+ [cell_ setUpWithCreditCard:card_
+ contentInjector:content_injector_
+ navigationDelegate:navigation_delegate_
+ menuActions:@[]
+ cellIndex:0
+ cellIndexAccessibilityLabel:@""
+ showAutofillFormButton:YES];
+ }
+
+ void TearDown() override {
+ [cell_ prepareForReuse];
+ cell_ = nil;
+ card_ = nil;
+ navigation_delegate_ = nil;
+ content_injector_ = nil;
+ PlatformTest::TearDown();
+ }
+
+ id<ManualFillContentInjector> content_injector_;
+ id<CardListDelegate> navigation_delegate_;
+ ManualFillCreditCard* card_;
+ ManualFillCardCell* cell_;
+};
+
+// Tests that tapping the "Autofill Form" button forwards the suggestion to the
+// content injector when injection is allowed.
+TEST_F(ManualFillCardCellTest, AutofillFormButtonForwardsSuggestion) {
+ OCMExpect([content_injector_ canUserInjectInPasswordField:NO
+ requiresHTTPS:YES])
+ .andReturn(YES);
+ OCMExpect([content_injector_ autofillFormWithSuggestion:[OCMArg any]
+ atIndex:0]);
+
+ [AutofillFormButton(cell_)
+ sendActionsForControlEvents:UIControlEventTouchUpInside];
+
+ EXPECT_OCMOCK_VERIFY(content_injector_);
+}
+
+// Tests that tapping the "Autofill Form" button does not forward the suggestion
+// to the content injector when injection is not allowed.
+TEST_F(ManualFillCardCellTest, AutofillFormButtonRespectsInjectionCheck) {
+ OCMExpect([content_injector_ canUserInjectInPasswordField:NO
+ requiresHTTPS:YES])
+ .andReturn(NO);
+ OCMReject([content_injector_ autofillFormWithSuggestion:[OCMArg any]
+ atIndex:0]);
+
+ [AutofillFormButton(cell_)
... (file diff truncated)
Regression Test / PoC
diff --git a/ios/chrome/browser/autofill/manual_fill/test/BUILD.gn b/ios/chrome/browser/autofill/manual_fill/test/BUILD.gn
index 8866c3e..81513ff 100644
--- a/ios/chrome/browser/autofill/manual_fill/test/BUILD.gn
+++ b/ios/chrome/browser/autofill/manual_fill/test/BUILD.gn
@@ -33,6 +33,7 @@
]
deps = [
":eg_test_support+eg2",
+ "//base",
"//components/autofill/core/browser:test_support",
"//components/password_manager/core/browser",
"//components/password_manager/core/browser/features:password_features",
diff --git a/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm b/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm
index f37e51f..b60f36e 100644
--- a/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm
+++ b/ios/chrome/browser/autofill/manual_fill/test/card_view_controller_egtest.mm
@@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#import <optional>
+
#import "base/ios/ios_util.h"
+#import "base/path_service.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/autofill/core/browser/test_utils/autofill_test_util.h"
@@ -23,6 +26,7 @@
#import "ios/testing/earl_grey/app_launch_configuration.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/public/test/element_selector.h"
+#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"
@@ -281,7 +285,9 @@
} // namespace
// Integration Tests for Manual Fallback credit cards View Controller.
-@interface CreditCardViewControllerTestCase : ChromeTestCase
+@interface CreditCardViewControllerTestCase : ChromeTestCase {
+ std::optional<net::test_server::EmbeddedTestServer> _HTTPSServer;
+}
@end
@implementation CreditCardViewControllerTestCase
@@ -295,6 +301,7 @@
- (void)setUp {
[super setUp];
+ _HTTPSServer.reset();
GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
const GURL URL = self.testServer->GetURL(kFormHTMLFile);
[self loadURL];
@@ -318,6 +325,7 @@
[MetricsAppInterface stopOverridingMetricsAndCrashReportingForTesting];
chrome_test_util::GREYAssertErrorNil(
[MetricsAppInterface releaseHistogramTester]);
+ _HTTPSServer.reset();
[super tearDownHelper];
}
@@ -1005,6 +1013,22 @@
[ReauthenticationAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
+ [self startHTTPSServer];
+ const GURL URL = _HTTPSServer->GetURL(kFormHTMLFile);
+ [ChromeEarlGrey loadURL:URL];
+
+ // Check if the SSL warning page is displayed by verifying if the
+ // "details-button" element exists on the page.
+ base::Value result = [ChromeEarlGrey
+ evaluateJavaScript:@"document.getElementById('details-button') !== null"];
+ if (result.is_bool() && result.GetBool()) {
+ [ChromeEarlGrey tapWebStateElementWithID:@"details-button"];
+ [ChromeEarlGrey tapWebStateElementWithID:@"proceed-link"];
+ }
+
+ [ChromeEarlGrey waitForWebStateContainingText:"Autofill Test"];
+ [AutofillAppInterface considerCreditCardFormSecureForTesting];
+
// Save a card.
[AutofillAppInterface saveLocalCreditCard];
@@ -1077,6 +1101,16 @@
[ChromeEarlGrey waitForWebStateContainingText:"Autofill Test"];
}
+// Starts the dedicated HTTPS test server.
+- (void)startHTTPSServer {
+ _HTTPSServer.emplace(net::test_server::EmbeddedTestServer::TYPE_HTTPS);
+ _HTTPSServer->ServeFilesFromDirectory(
+ base::PathService::CheckedGet(base::DIR_ASSETS)
+ .AppendASCII("ios/testing/data/http_server_files/"));
+ net::test_server::RegisterDefaultHandlers(&_HTTPSServer.value());
+ GREYAssertTrue(_HTTPSServer->Start(), @"HTTPS Test server failed to start.");
+}
+
- (void)verifyCreditCardButtonWithTitle:(NSString*)title
doesInjectValue:(NSString*)result {
[AutofillAppInterface saveLocalCreditCard];
diff --git a/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm
new file mode 100644
index 0000000..218e571
--- /dev/null
+++ b/ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell_unittest.mm
@@ -0,0 +1,330 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.h"
+
+#import "ios/chrome/browser/autofill/manual_fill/model/manual_fill_credit_card.h"
+#import "ios/chrome/browser/autofill/manual_fill/public/manual_fill_content_injector.h"
+#import "ios/chrome/browser/autofill/manual_fill/ui/card_list_delegate.h"
+#import "ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell+Testing.h"
+#import "testing/gtest_mac.h"
+#import "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+#import "third_party/ocmock/gtest_support.h"
+
+namespace {
+
+// Returns a card sufficient to populate a `ManualFillCardCell`.
+ManualFillCreditCard* TestLocalCard() {
+ return [[ManualFillCreditCard alloc]
+ initWithGUID:@"00000000-0000-0000-0000-000000000001"
+ network:@"Visa"
+ icon:nil
+ bankName:nil
+ cardHolder:@"Test User"
+ number:@"4111111111111111"
+ obfuscatedNumber:@"••••1111"
+ networkAndLastFourDigits:nil
+ expirationYear:@"2100"
+ expirationMonth:@"01"
+ CVC:nil
+ recordType:autofill::CreditCard::RecordType::
+ kLocalCard
+ cardInfoRetrievalEnrollmentState:
+ autofill::CreditCard::CardInfoRetrievalEnrollmentState::
+ kRetrievalUnenrolledAndNotEligible
+ canFillDirectly:YES];
+}
+
+// Returns a masked server card that cannot be filled directly.
+ManualFillCreditCard* TestServerCard() {
+ return [[ManualFillCreditCard alloc]
+ initWithGUID:@"00000000-0000-0000-0000-000000000002"
+ network:@"Visa"
+ icon:nil
+ bankName:nil
+ cardHolder:@"Test User"
+ number:@"4111111111111111"
+ obfuscatedNumber:@"••••1111"
+ networkAndLastFourDigits:nil
+ expirationYear:@"2100"
+ expirationMonth:@"01"
+ CVC:@"•••"
+ recordType:autofill::CreditCard::RecordType::
+ kMaskedServerCard
+ cardInfoRetrievalEnrollmentState:
+ autofill::CreditCard::CardInfoRetrievalEnrollmentState::
+ kRetrievalUnenrolledAndNotEligible
+ canFillDirectly:NO];
+}
+
+// Returns a virtual card.
+ManualFillCreditCard* TestVirtualCard() {
+ return [[ManualFillCreditCard alloc]
+ initWithGUID:@"00000000-0000-0000-0000-000000000003"
+ network:@"Visa"
+ icon:nil
+ bankName:nil
+ cardHolder:@"Test User"
+ number:@"4111111111111111"
+ obfuscatedNumber:@"••••1111"
+ networkAndLastFourDigits:nil
+ expirationYear:@"2100"
+ expirationMonth:@"01"
+ CVC:nil
+ recordType:autofill::CreditCard::RecordType::
+ kVirtualCard
+ cardInfoRetrievalEnrollmentState:
+ autofill::CreditCard::CardInfoRetrievalEnrollmentState::
+ kRetrievalUnenrolledAndNotEligible
+ canFillDirectly:NO];
+}
+
+// Returns the "Autofill Form" button of `cell`.
+UIButton* AutofillFormButton(ManualFillCardCell* cell) {
+ return cell.autofillFormButton;
+}
+
+// Returns the CVC button of `cell`.
+UIButton* CvcButton(ManualFillCardCell* cell) {
+ return cell.cvcButton;
+}
+
+// Returns the expiration month button of `cell`.
+UIButton* ExpirationMonthButton(ManualFillCardCell* cell) {
+ return cell.expirationMonthButton;
+}
+
+// Returns the expiration year button of `cell`.
+UIButton* ExpirationYearButton(ManualFillCardCell* cell) {
+ return cell.expirationYearButton;
+}
+
+} // namespace
+
+class ManualFillCardCellTest : public PlatformTest {
+ protected:
+ void SetUp() override {
+ PlatformTest::SetUp();
+ content_injector_ = OCMProtocolMock(@protocol(ManualFillContentInjector));
+ navigation_delegate_ = OCMProtocolMock(@protocol(CardListDelegate));
+ card_ = TestLocalCard();
+ cell_ = [[ManualFillCardCell alloc] init];
+ [cell_ setUpWithCreditCard:card_
+ contentInjector:content_injector_
+ navigationDelegate:navigation_delegate_
+ menuActions:@[]
+ cellIndex:0
+ cellIndexAccessibilityLabel:@""
+ showAutofillFormButton:YES];
+ }
+
+ void TearDown() override {
+ [cell_ prepareForReuse];
+ cell_ = nil;
+ card_ = nil;
+ navigation_delegate_ = nil;
+ content_injector_ = nil;
+ PlatformTest::TearDown();
+ }
+
+ id<ManualFillContentInjector> content_injector_;
+ id<CardListDelegate> navigation_delegate_;
+ ManualFillCreditCard* card_;
+ ManualFillCardCell* cell_;
+};
+
+// Tests that tapping the "Autofill Form" button forwards the suggestion to the
+// content injector when injection is allowed.
+TEST_F(ManualFillCardCellTest, AutofillFormButtonForwardsSuggestion) {
+ OCMExpect([content_injector_ canUserInjectInPasswordField:NO
+ requiresHTTPS:YES])
+ .andReturn(YES);
+ OCMExpect([content_injector_ autofillFormWithSuggestion:[OCMArg any]
+ atIndex:0]);
+
+ [AutofillFormButton(cell_)
+ sendActionsForControlEvents:UIControlEventTouchUpInside];
+
+ EXPECT_OCMOCK_VERIFY(content_injector_);
+}
+
+// Tests that tapping the "Autofill Form" button does not forward the suggestion
+// to the content injector when injection is not allowed.
+TEST_F(ManualFillCardCellTest, AutofillFormButtonRespectsInjectionCheck) {
+ OCMExpect([content_injector_ canUserInjectInPasswordField:NO
+ requiresHTTPS:YES])
+ .andReturn(NO);
+ OCMReject([content_injector_ autofillFormWithSuggestion:[OCMArg any]
+ atIndex:0]);
+
+ [AutofillFormButton(cell_)
+ sendActionsForControlEvents:UIControlEventTouchUpInside];
+
+ EXPECT_OCMOCK_VERIFY(content_injector_);
+}
+
+// Tests that tapping the CVC button for a server card does not request the full
+// credit card when injection is not allowed.
+TEST_F(ManualFillCardCellTest, CvcButtonForServerCardRespectsInjectionCheck) {
+ ManualFillCreditCard* server_card = TestServerCard();
+ [cell_ prepareForReuse];
+ [cell_ setUpWithCreditCard:server_card
+ contentInjector:content_injector_
+ navigationDelegate:navigation_delegate_
+ menuActions:@[]
+ cellIndex:0
+ cellIndexAccessibilityLabel:@""
+ showAutofillFormButton:YES];
+
+ OCMExpect([content_injector_ canUserInjectInPasswordField:NO
+ requiresHTTPS:YES])
+ .andReturn(NO);
+ OCMReject([navigation_delegate_
+ requestFullCreditCard:[OCMArg any]
+ fieldType:manual_fill::PaymentFieldType::kCVC]);
+
+ [CvcButton(cell_) sendActionsForControlEvents:UIControlEventTouchUpInside];
+
+ EXPECT_OCMOCK_VERIFY(content_injector_);
+ EXPECT_OCMOCK_VERIFY(navigation_delegate_);
+}
... (truncated)
Original Bug Report
Insecure credit card filling on iOS Chrome manual fallback via IBAN field focus pivot
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential security context bypass in iOS Chrome’s manual-fill keyboard accessory allows credit card information to be filled into insecure HTTP forms. When the ‘Autofill Form’ button on a credit card cell is tapped, context security checks are omitted on the native injection path. This can potentially be exploited by an attacker using an IBAN field focus pivot to bypass upstream secure context filters.
Affected files:
ios/chrome/browser/autofill/manual_fill/model/manual_fill_injection_handler.mmios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mmios/chrome/browser/autofill/manual_fill/coordinator/expanded_manual_fill_coordinator.mm
Estimated timestamp from git blame: 2024-05-08
Summary
A potential security context bypass in iOS Chrome’s manual-fill keyboard accessory allows credit card information to be filled into insecure HTTP forms. The manual-fill injection handler for the “Autofill Form” button on credit card cells fails to verify if the frame is a secure context (requiresHTTPS), unlike individual field chips and credential autofill handlers.
An attacker can potentially bypass upstream secure context filters (which usually prevent credit card suggestions on HTTP pages) by hosting both an IBAN field and credit card fields on an insecure HTTP page.
Technical Analysis
1. Root Cause in Manual Fill Injection
When a user taps the “Autofill Form” button in the expanded manual-fill sheet, -[ManualFillCardCell onAutofillFormButtonTapped] (in ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mm at line 655) generates a FormSuggestion with a credit card type and passes it to the content injector:
[self.contentInjector autofillFormWithSuggestion:suggestion atIndex:_cellIndex];
The receiving method, -[ManualFillInjectionHandler autofillFormWithSuggestion:atIndex:] (in ios/chrome/browser/autofill/manual_fill/model/manual_fill_injection_handler.mm at line 222), delegates the injection directly to the FormSuggestionClient without verifying context security:
- (void)autofillFormWithSuggestion:(FormSuggestion*)formSuggestion
atIndex:(NSInteger)index {
if (IsStateless()) {
...
[self.formSuggestionClient didSelectSuggestion:decoratedSuggestion ...];
} else {
[self.formSuggestionClient didSelectSuggestion:formSuggestion ...];
}
}
By contrast, individual card chip selection is properly gated:
// ios/chrome/browser/autofill/manual_fill/ui/manual_fill_card_cell.mm
- (void)userDidTapCardNumber:(UIButton*)sender {
...
if (![self.contentInjector canUserInjectInPasswordField:NO
requiresHTTPS:YES]) {
return;
}
Similarly, password-based form autofill is gated in the same injection handler (line 200):
- (void)autofillFormWithCredential:(ManualFillCredential*)credential
shouldReauth:(BOOL)shouldReauth {
if (![self canUserInjectInPasswordField:NO requiresHTTPS:YES]) {
return;
}
Because downstream handlers (AutofillAgent, AutofillExternalDelegate, and BrowserAutofillManager) perform no secondary IsFormOrClientNonSecure validation on the selection-handling flow (as they rely on suggestions being filtered out upstream), the data is directly injected into the page once accepted.
2. Upstream Bypass via IBAN Pivot
Ordinarily, CreditCardSuggestionGenerator::GenerateSuggestions (in components/autofill/core/browser/suggestions/payments/credit_card_suggestion_generator.cc at line 568) replaces credit card suggestions with a warning block on insecure pages, which hides the credit card “Autofill Form” button.
However, IbanSuggestionGenerator::GenerateSuggestions (in components/autofill/core/browser/suggestions/payments/iban_suggestion_generator.cc at line 145) does not check for secure contexts. Thus, on an insecure page containing both an IBAN field and credit card fields, an attacker can potentially trigger the vulnerable path:
- The user focuses the IBAN field on an HTTP page, generating valid IBAN suggestions.
- The main filling product maps to
ManualFillDataType::kPaymentMethodinios/chrome/browser/autofill/manual_fill/coordinator/expanded_manual_fill_coordinator.mm(at line 249), which configures the manual fill sheet withshowAutofillFormButton:YESfor the credit card cells. - The user opens the manual fill sheet and selects their credit card from the list.
- Tapping the “Autofill Form” button triggers the ungated injection pathway, filling the credit card fields on the insecure HTTP page.
Note: These are potential steps. Our tooling agent does not yet have the ability to run code or compile verification binaries on target devices.
Potential Steps to Reproduce
- Ensure the user has a saved local credit card and a saved IBAN in their Google Chrome profile.
- Navigate to an insecure HTTP page (e.g.,
http://example.com) hosting both an IBAN input field and credit card input fields. - Tap on the IBAN input field.
- Open the keyboard accessory’s manual fill menu and select the Payment Methods tab.
- Tap the “Autofill Form” button on the saved credit card cell.
- Verify that the credit card information is successfully filled into the insecure HTTP form.
Suggested Fix
Enforce the secure context check in -[ManualFillInjectionHandler autofillFormWithSuggestion:atIndex:] before letting the client execute the injection:
- (void)autofillFormWithSuggestion:(FormSuggestion*)formSuggestion
atIndex:(NSInteger)index {
if (![self canUserInjectInPasswordField:NO requiresHTTPS:YES]) {
return;
}
...
}
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.