CVE-2026-95336
Overview
Background
- Autocomplete history
- Chrome’s local store of previously typed form-field values that are offered as suggestions on later visits, managed by
AutocompleteHistoryManager. - Field rationalization
- the Autofill process that assigns a semantic type to each form field, which can legitimately fall back to
UNKNOWN_TYPEfor some credit-card fields. - `html_type()`
- the
AutofillFieldaccessor exposing the field’s parsed HTMLautocompleteattribute as anHtmlFieldType(e.g.kCreditCardVerificationCodeforcc-csc). - Sensitive field types
- CVCs, credit-card numbers, IBANs, and promo codes, which must never be persisted to autocomplete history.
Root Cause Analysis
Before the fix, eligibility to persist a field value to autocomplete history was decided solely by IsFieldTypeSaveable, which looked at the field’s predicted (rationalized) type. Because credit-card fields can legitimately rationalize to UNKNOWN_TYPE, a field carrying autocomplete="cc-csc" (a CVC) could slip past the predicted-type check and be treated as saveable. The invariant that sensitive payment data is never written to autocomplete history was therefore violated whenever the predicted type failed to identify the field.
The fix renames the predicted-type check to IsPredictedFieldTypeSaveable and adds a parallel IsHtmlFieldTypeSaveable that rejects fields whose html_type() is kCreditCardVerificationCode, kCreditCardNumber, kIban, or kMerchantPromoCode, regardless of prediction. Both checks now gate persistence in IsValidFieldDataForAutocompleteHistory, so a CVC tagged only by its HTML attribute is caught by the second layer.
UNKNOWN_TYPE; the fix adds a second, attribute-based gate (IsHtmlFieldTypeSaveable) so the HTML autocomplete value alone is sufficient to reject a CVC or other sensitive field.Attack Path
- Craft a mislabeled CVC field
An attacker page (or a site whose markup confuses the parser) presents an input with
autocomplete="cc-csc"but a name that does not match the credit-card regex, so Autofill rationalizes it toUNKNOWN_TYPE. - Bypass the predicted-type filter
Because
IsFieldTypeSaveablekeys off the predicted type, theUNKNOWN_TYPEfield passes the saveability check that was meant to exclude CVCs. - CVC persisted to history The user’s entered CVC value is stored as an autocomplete history entry, since no HTML-attribute-based check existed to reject it.
- Value re-surfaced later On a subsequent visit, the stored CVC is offered as an autocomplete suggestion, exposing the sensitive value in the field’s local history where it should never appear. (inferred)
Impact Assessment
UNKNOWN_TYPE while carrying a sensitive autocomplete attribute. The advisory classes this as an information leak in the Transactions Platform (LogicError) and states no exploited-in-the-wild status. Any broader exfiltration beyond local history exposure is not demonstrated by the patch. (inferred)Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc |
modified | |
TEST_Fcomponents/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc |
modified |
Files Changed
components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cccomponents/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc
Audit Directions
- Prediction-only sensitivity gatesLook for any code that decides to store, log, or expose form values based solely on the rationalized/predicted field type, and add a fallback keyed on
html_type()for cases that rationalize toUNKNOWN_TYPE. - Sensitive-type enumerationsAudit every place that lists sensitive types (CVC, card number, IBAN, promo code) to ensure the HTML-attribute and predicted-type variants are kept in sync as new
HtmlFieldTypevalues are added. - Other single-field fillersReview sibling fillers under
components/autofill/core/browser/single_field_fillers/(e.g. IBAN and promo-code managers) for the same predicted-vs-HTML-type discrepancy in their persistence checks.
Patch
From 2b02287549c6fe7ec4e00ba1f4fa31487017c98c Mon Sep 17 00:00:00 2001 From: Grzegorz (Greg) Grzelachowski <[email protected]> Date: Wed, 26 Aug 2026 06:57:08 -0700 Subject: [PATCH] [Autofill] Disable autocomplete based on `autocomplete` attribute (e.g. CVC) There are valid code paths that might result in Credit Card fields being rationalized to `UNKNOWN_TYPE`. This has negative effect on the autocomplete feature, where fields marked as `cc-csc` could be persisted. Bug: 532962621 Change-Id: Ia90d63201583f7bf28e72264e90eb3906a6a6964 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8239572 Reviewed-by: Gianmarco Picarella <[email protected]> Commit-Queue: Grzegorz (Greg) Grzelachowski <[email protected]> Reviewed-by: Jihad Hanna <[email protected]> Cr-Commit-Position: refs/heads/main@{#1686352} --- diff --git a/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc b/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc index 9fabb60..b3ad834 100644 --- a/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc +++ b/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc @@ -55,8 +55,7 @@ namespace { // Returns true if the field type is eligible to be saved in the autocomplete // history. Some types (promo codes, IBANs, CCs, CVCs) are excluded. -bool IsFieldTypeSaveable(const FormStructure* form, FieldGlobalId field_id) { - const AutofillField* field = form ? form->GetFieldById(field_id) : nullptr; +bool IsPredictedFieldTypeSaveable(const AutofillField* field) { if (!field) { return true; } @@ -204,6 +203,69 @@ return true; } +// An equivalent of `IsPredictedFieldTypeSaveable` that operates on the values +// of the HTML autocomplete attribute. It serves as an additional validation +// e.g. for cases when predicted type is `UNKNOWN_TYPE`. +bool IsHtmlFieldTypeSaveable(const AutofillField* field) { + if (!field) { + return true; + } + switch (field->html_type()) { + case HtmlFieldType::kCreditCardVerificationCode: + case HtmlFieldType::kCreditCardNumber: + case HtmlFieldType::kIban: + case HtmlFieldType::kMerchantPromoCode: + return false; + case HtmlFieldType::kUnspecified: + case HtmlFieldType::kName: + case HtmlFieldType::kHonorificPrefix: + case HtmlFieldType::kGivenName: + case HtmlFieldType::kAdditionalName: + case HtmlFieldType::kFamilyName: + case HtmlFieldType::kOrganization: + case HtmlFieldType::kStreetAddress: + case HtmlFieldType::kAddressLine1: + case HtmlFieldType::kAddressLine2: + case HtmlFieldType::kAddressLine3: + case HtmlFieldType::kAddressLevel1: + case HtmlFieldType::kAddressLevel2: + case HtmlFieldType::kAddressLevel3: + case HtmlFieldType::kCountryCode: + case HtmlFieldType::kCountryName: + case HtmlFieldType::kPostalCode: + case HtmlFieldType::kCreditCardNameFull: + case HtmlFieldType::kCreditCardNameFirst: + case HtmlFieldType::kCreditCardNameLast: + case HtmlFieldType::kCreditCardExp: + case HtmlFieldType::kCreditCardExpMonth: + case HtmlFieldType::kCreditCardExpYear: + case HtmlFieldType::kCreditCardType: + case HtmlFieldType::kTel: + case HtmlFieldType::kTelCountryCode: + case HtmlFieldType::kTelNational: + case HtmlFieldType::kTelAreaCode: + case HtmlFieldType::kTelLocal: + case HtmlFieldType::kTelLocalPrefix: + case HtmlFieldType::kTelLocalSuffix: + case HtmlFieldType::kTelExtension: + case HtmlFieldType::kEmail: + case HtmlFieldType::kBirthdateDay: + case HtmlFieldType::kBirthdateMonth: + case HtmlFieldType::kBirthdateYear: + case HtmlFieldType::kTransactionAmount: + case HtmlFieldType::kTransactionCurrency: + case HtmlFieldType::kAdditionalNameInitial: + case HtmlFieldType::kCreditCardExpDate2DigitYear: + case HtmlFieldType::kCreditCardExpDate4DigitYear: + case HtmlFieldType::kCreditCardExp2DigitYear: + case HtmlFieldType::kCreditCardExp4DigitYear: + case HtmlFieldType::kOneTimeCode: + case HtmlFieldType::kUnrecognized: + return true; + } + NOTREACHED(); +} + // Returns true if the given `field` in `form` and its value are valid to be // saved as a new or updated Autocomplete entry. // We put the following restriction on stored FormFields: @@ -247,14 +309,20 @@ return false; } + const AutofillField* autofill_field = + form ? form->GetFieldById(field.global_id()) : nullptr; + // Reject fields with types that are ineligible for autocomplete such as // credit card numbers, CVCs, IBANs, or promo codes. - if (!IsFieldTypeSaveable(form, field.global_id())) { + if (!IsPredictedFieldTypeSaveable(autofill_field)) { return false; } - const AutofillField* autofill_field = - form ? form->GetFieldById(field.global_id()) : nullptr; + // Reject fields with HTML types that are ineligible for autocomplete. + if (!IsHtmlFieldTypeSaveable(autofill_field)) { + return false; + } + if (autofill_field && autofill_field->all_modifiers().contains(FieldModifier::kAutofill) && (autofill_field->last_modifier() != FieldModifier::kUser || diff --git a/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc b/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc index 460ab33..cdaa76a0 100644 --- a/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc +++ b/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc @@ -34,6 +34,7 @@ #include "components/autofill/core/common/autofill_prefs.h" #include "components/autofill/core/common/autofill_test_utils.h" #include "components/autofill/core/common/form_data.h" +#include "components/autofill/core/common/html_field_types.h" #include "components/optimization_guide/core/feature_registry/feature_registration.h" #include "components/optimization_guide/core/model_execution/model_execution_prefs.h" #include "components/personal_context/core/mock_personal_context_eligibility_service.h" @@ -1053,6 +1054,33 @@ &form_structure); } +// Tests that CVC fields tagged through autocomplete attribute are not saved in +// history even when the type wasn't predicted. +TEST_F(AutocompleteHistoryManagerTest, + AutocompleteAttributeBasedFiltering_CvcWithoutTypePrediction) { + FormData form = + test::GetFormData({.fields = {{.role = CREDIT_CARD_VERIFICATION_CODE, + .name = u"Name not matching CC regex", + .value = u"000", + .autocomplete_attribute = "cc-csc"}, + {.role = CREDIT_CARD_VERIFICATION_CODE, + .name = u"Name not matching CC regex", + .value = u"111"}}}); + FormStructure form_structure{form}; + // Simulate rationalizing the types to UNKNOWN_TYPE. + form_structure.field(0)->SetTypeTo( + AutofillType(UNKNOWN_TYPE), AutofillPredictionSource::kRationalization); + form_structure.field(1)->SetTypeTo( + AutofillType(UNKNOWN_TYPE), AutofillPredictionSource::kRationalization); + + EXPECT_CALL(*(web_data_service_.get()), + AddFormFields(testing::ElementsAre( + testing::Property(&FormFieldData::value, u"111")))); + + autocomplete_manager_->OnWillSubmitFormWithFields(form.fields(), + &form_structure); +} + // Tests that loyalty card fields are saved in autocomplete history if they // were entered manually (i.e., not autofilled). TEST_F(AutocompleteHistoryManagerTest, LoyaltyCardManualEntryIsSaved) {
Regression Test / PoC
diff --git a/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc b/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc
index 460ab33..cdaa76a0 100644
--- a/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc
+++ b/components/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager_unittest.cc
@@ -34,6 +34,7 @@
#include "components/autofill/core/common/autofill_prefs.h"
#include "components/autofill/core/common/autofill_test_utils.h"
#include "components/autofill/core/common/form_data.h"
+#include "components/autofill/core/common/html_field_types.h"
#include "components/optimization_guide/core/feature_registry/feature_registration.h"
#include "components/optimization_guide/core/model_execution/model_execution_prefs.h"
#include "components/personal_context/core/mock_personal_context_eligibility_service.h"
@@ -1053,6 +1054,33 @@
&form_structure);
}
+// Tests that CVC fields tagged through autocomplete attribute are not saved in
+// history even when the type wasn't predicted.
+TEST_F(AutocompleteHistoryManagerTest,
+ AutocompleteAttributeBasedFiltering_CvcWithoutTypePrediction) {
+ FormData form =
+ test::GetFormData({.fields = {{.role = CREDIT_CARD_VERIFICATION_CODE,
+ .name = u"Name not matching CC regex",
+ .value = u"000",
+ .autocomplete_attribute = "cc-csc"},
+ {.role = CREDIT_CARD_VERIFICATION_CODE,
+ .name = u"Name not matching CC regex",
+ .value = u"111"}}});
+ FormStructure form_structure{form};
+ // Simulate rationalizing the types to UNKNOWN_TYPE.
+ form_structure.field(0)->SetTypeTo(
+ AutofillType(UNKNOWN_TYPE), AutofillPredictionSource::kRationalization);
+ form_structure.field(1)->SetTypeTo(
+ AutofillType(UNKNOWN_TYPE), AutofillPredictionSource::kRationalization);
+
+ EXPECT_CALL(*(web_data_service_.get()),
+ AddFormFields(testing::ElementsAre(
+ testing::Property(&FormFieldData::value, u"111"))));
+
+ autocomplete_manager_->OnWillSubmitFormWithFields(form.fields(),
+ &form_structure);
+}
+
// Tests that loyalty card fields are saved in autocomplete history if they
// were entered manually (i.e., not autofilled).
TEST_F(AutocompleteHistoryManagerTest, LoyaltyCardManualEntryIsSaved) {