CVE-2026-17879
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/autofill/core/browser/foundations/browser_autofill_manager.cc |
modified | |
TEST_Fcomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc |
modified |
Files Changed
components/autofill/core/browser/foundations/browser_autofill_manager.cccomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cccomponents/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc
Patch
From 99815e08c6040f6f7f524da20e3af56117e4dc09 Mon Sep 17 00:00:00 2001
From: Gianmarco Picarella <picarella@google.com>
Date: Fri, 26 Jun 2026 02:48:32 -0700
Subject: [PATCH] Prevent CVC leakage to autocomplete
This CL implements mitigations to prevent CVCs from being stored in the
autocomplete database under two vulnerability scenarios:
1. Travel parser priority inversion: On travel booking sites, a CVC
field named "flight_verification" matches both the Travel parser (via
"flight") and the Credit Card parser (via "verification"). Because the
Travel parser has higher priority, the field is classified as
UNKNOWN_TYPE, bypassing autocomplete suppression.
2. Standalone CVC email-skip: Standalone CVC parsing is skipped if an
email field is present, causing the CVC field to remain UNKNOWN_TYPE.
Even on forms without email where it is successfully classified as
CREDIT_CARD_STANDALONE_VERIFICATION_CODE, the autocomplete import check
failed to suppress standalone CVCs.
This CL tries to address both issues by implementing the following
changes:
- Updates `BrowserAutofillManager::MaybeImportFromSubmittedForm()` to
suppress autocomplete for both `CREDIT_CARD_VERIFICATION_CODE` and
`CREDIT_CARD_STANDALONE_VERIFICATION_CODE` types.
- Moves the logic to suppress autocomplete for
`CREDIT_CARD_VERIFICATION_CODE`
`CREDIT_CARD_STANDALONE_VERIFICATION_CODE`, `IBAN_VALUE`,
`MERCHANT_PROMO_CODE` and `LOYALTY_MEMBERSHIP_ID` types into
`autocomplete_history_manager.cc` and adds specific unit tests for it.
Regexes in
`AutocompleteHistoryManager::IsFieldNameMeaningfulForAutocomplete()`
will be expanded in a follow-up CL.
Bug: 523229759, 522878450, 40100455
Change-Id: I46bfc1b389f3856c11a28e0d56aad8193a16e6a4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7967519
Reviewed-by: Karol Sygiet <sygiet@google.com>
Reviewed-by: Jihad Hanna <jihadghanna@google.com>
Commit-Queue: Gianmarco Picarella <picarella@google.com>
Cr-Commit-Position: refs/heads/main@{#1653012}
---
diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager.cc b/components/autofill/core/browser/foundations/browser_autofill_manager.cc
index 7e1a084..687336b 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager.cc
@@ -625,49 +625,24 @@
// Triggers the possible import of submitted data at submission time.
void MaybeImportFromSubmittedForm(AutofillClient& client,
ukm::SourceId ukm_source_id,
- const FormStructure& form_structure) {
+ const FormStructure& form) {
// This intentionally happens prior to `ImportAndProcessFormData()`. See
// crbug.com/381205586.
ProfileTokenQuality::SaveObservationsForFilledFormForAllSubmittedProfiles(
- form_structure, client.GetPersonalDataManager().address_data_manager());
+ form, client.GetPersonalDataManager().address_data_manager());
AutofillAiManager* const ai_manager = client.GetAutofillAiManager();
const bool autofill_ai_shows_bubble =
- ai_manager && ai_manager->OnFormSubmitted(form_structure, ukm_source_id);
+ ai_manager && ai_manager->OnFormSubmitted(form, ukm_source_id);
if (!autofill_ai_shows_bubble) {
// Update Personal Data with the form's submitted data.
client.GetFormDataImporter()->ImportAndProcessFormData(
- form_structure, client.IsAutofillProfileEnabled(),
+ form, client.IsAutofillProfileEnabled(),
client.GetPaymentsAutofillClient()->IsAutofillPaymentMethodsEnabled(),
ukm_source_id);
}
-
- std::vector<FormFieldData> fields_for_autocomplete = base::ToVector(
- form_structure,
- [&](const std::unique_ptr<AutofillField>& autofill_field) {
- FormFieldData field = *autofill_field;
- FieldType cc_type = autofill_field->Type().GetCreditCardType();
- if (cc_type == CREDIT_CARD_VERIFICATION_CODE ||
- cc_type == CREDIT_CARD_STANDALONE_VERIFICATION_CODE) {
- // However, if Autofill has recognized a field as CVC, that shouldn't
- // be saved.
- field.set_should_autocomplete(false);
- }
- if (autofill_field->Type().GetLoyaltyCardType() ==
- LOYALTY_MEMBERSHIP_ID &&
- autofill_field->last_modifier() == FieldModifier::kAutofill) {
- // Only store loyalty cards values in Autocomplete if they were filled
- // manually.
- field.set_should_autocomplete(false);
- }
- return field;
- });
-
- // TODO crbug.com/40100455 - Eliminate `form_for_autocomplete`.
- FormData form_for_autocomplete = form_structure.ToFormData();
- form_for_autocomplete.set_fields(std::move(fields_for_autocomplete));
client.GetSingleFieldFillRouter().OnWillSubmitForm(
- form_for_autocomplete, &form_structure, client.IsAutocompleteEnabled());
+ form.ToFormData(), &form, client.IsAutocompleteEnabled());
}
// Generates a compose suggestion for the given `form` and `field` if conditions
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 33c281f0..90f5476 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
@@ -4350,72 +4350,6 @@
EXPECT_TRUE(external_delegate()->on_suggestions_returned_seen());
}
-// Test that inputs detected to be CVC inputs are forced to
-// !should_autocomplete for SingleFieldFillRouter::OnWillSubmitForm.
-TEST_F(BrowserAutofillManagerTest, DontSaveCvcInAutocompleteHistory) {
- FormData form_seen_by_ahm;
- EXPECT_CALL(single_field_fill_router(), OnWillSubmitForm(_, _, true))
- .WillOnce(SaveArg<0>(&form_seen_by_ahm));
-
- FormData form = test::GetFormData(
- {.fields = {
- {.role = CREDIT_CARD_NUMBER, .value = u"4234-5678-9012-3456"},
- {.role = CREDIT_CARD_VERIFICATION_CODE, .value = u"123"},
- {.role = CREDIT_CARD_EXP_4_DIGIT_YEAR, .value = u"04/2020"}}});
-
- FormsSeen({form});
- FormSubmitted(form);
-
- EXPECT_EQ(form.fields().size(), form_seen_by_ahm.fields().size());
- ASSERT_EQ(3u, form_seen_by_ahm.fields().size());
- EXPECT_TRUE(form_seen_by_ahm.fields()[0].should_autocomplete());
- EXPECT_FALSE(form_seen_by_ahm.fields()[1].should_autocomplete());
- EXPECT_TRUE(form_seen_by_ahm.fields()[2].should_autocomplete());
-}
-
-// Test that inputs detected to be standalone CVC inputs are forced to
-// !should_autocomplete for SingleFieldFillRouter::OnWillSubmitForm.
-TEST_F(BrowserAutofillManagerTest, DontSaveStandaloneCvcInAutocompleteHistory) {
- FormData form_seen_by_ahm;
- EXPECT_CALL(single_field_fill_router(),
- OnWillSubmitForm(_, _, /*is_autocomplete_enabled=*/true))
- .WillOnce(SaveArg<0>(&form_seen_by_ahm));
-
- FormData form = test::GetFormData(
- {.fields = {{.role = CREDIT_CARD_STANDALONE_VERIFICATION_CODE,
- .value = u"123"}}});
- autofill_manager().AddSeenForm(form,
- {CREDIT_CARD_STANDALONE_VERIFICATION_CODE});
- FormSubmitted(form);
-
- ASSERT_EQ(1u, form_seen_by_ahm.fields().size());
- EXPECT_FALSE(form_seen_by_ahm.fields()[0].should_autocomplete());
-}
-
-// Test that autofilled loyalty card fields are forced to !should_autocomplete.
-TEST_F(BrowserAutofillManagerTest,
- DontSaveAutofilledLoyaltyCardsInAutocompleteHistory) {
- FormData form_seen_by_ahm;
- EXPECT_CALL(single_field_fill_router(), OnWillSubmitForm(_, _, true))
- .WillOnce(SaveArg<0>(&form_seen_by_ahm));
-
- // Set up form.
- FormData form = test::GetFormData({.fields = {
- {.role = LOYALTY_MEMBERSHIP_ID},
- }});
- autofill_manager().AddSeenForm(form, {LOYALTY_MEMBERSHIP_ID});
- // Mark the loyalty card field as autofilled.
- test_api(autofill_manager())
- .FindCachedFormById(form.global_id())
- ->field(0)
- ->AddFieldModifier(FieldModifier::kAutofill);
- test_api(form).field(0).set_value(u"LOYALTYCARDNUMBER");
-
- FormSubmitted(form);
- ASSERT_EQ(form.fields().size(), form_seen_by_ahm.fields().size());
- EXPECT_FALSE(test_api(form_seen_by_ahm).field(0).should_autocomplete());
-}
-
// Regression test for crbug.com/428900385.
TEST_F(BrowserAutofillManagerTest, NullAutofillFieldDoesNotCrash) {
FormData form = test::GetFormData({.fields = {
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 0101d11f..dc08f16 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
@@ -23,6 +23,7 @@
#include "base/version_info/version_info.h"
#include "components/autofill/core/browser/at_memory/at_memory_enablement_utils.h"
#include "components/autofill/core/browser/data_quality/validation.h"
+#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/foundations/autofill_client.h"
#include "components/autofill/core/browser/metrics/autofill_metrics.h"
@@ -47,6 +48,162 @@
namespace autofill {
+namespace {
+// Returns true if the field type is eligible to be saved in the autocomplete
Regression Test / PoC
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 33c281f0..90f5476 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
@@ -4350,72 +4350,6 @@
EXPECT_TRUE(external_delegate()->on_suggestions_returned_seen());
}
-// Test that inputs detected to be CVC inputs are forced to
-// !should_autocomplete for SingleFieldFillRouter::OnWillSubmitForm.
-TEST_F(BrowserAutofillManagerTest, DontSaveCvcInAutocompleteHistory) {
- FormData form_seen_by_ahm;
- EXPECT_CALL(single_field_fill_router(), OnWillSubmitForm(_, _, true))
- .WillOnce(SaveArg<0>(&form_seen_by_ahm));
-
- FormData form = test::GetFormData(
- {.fields = {
- {.role = CREDIT_CARD_NUMBER, .value = u"4234-5678-9012-3456"},
- {.role = CREDIT_CARD_VERIFICATION_CODE, .value = u"123"},
- {.role = CREDIT_CARD_EXP_4_DIGIT_YEAR, .value = u"04/2020"}}});
-
- FormsSeen({form});
- FormSubmitted(form);
-
- EXPECT_EQ(form.fields().size(), form_seen_by_ahm.fields().size());
- ASSERT_EQ(3u, form_seen_by_ahm.fields().size());
- EXPECT_TRUE(form_seen_by_ahm.fields()[0].should_autocomplete());
- EXPECT_FALSE(form_seen_by_ahm.fields()[1].should_autocomplete());
- EXPECT_TRUE(form_seen_by_ahm.fields()[2].should_autocomplete());
-}
-
-// Test that inputs detected to be standalone CVC inputs are forced to
-// !should_autocomplete for SingleFieldFillRouter::OnWillSubmitForm.
-TEST_F(BrowserAutofillManagerTest, DontSaveStandaloneCvcInAutocompleteHistory) {
- FormData form_seen_by_ahm;
- EXPECT_CALL(single_field_fill_router(),
- OnWillSubmitForm(_, _, /*is_autocomplete_enabled=*/true))
- .WillOnce(SaveArg<0>(&form_seen_by_ahm));
-
- FormData form = test::GetFormData(
- {.fields = {{.role = CREDIT_CARD_STANDALONE_VERIFICATION_CODE,
- .value = u"123"}}});
- autofill_manager().AddSeenForm(form,
- {CREDIT_CARD_STANDALONE_VERIFICATION_CODE});
- FormSubmitted(form);
-
- ASSERT_EQ(1u, form_seen_by_ahm.fields().size());
- EXPECT_FALSE(form_seen_by_ahm.fields()[0].should_autocomplete());
-}
-
-// Test that autofilled loyalty card fields are forced to !should_autocomplete.
-TEST_F(BrowserAutofillManagerTest,
- DontSaveAutofilledLoyaltyCardsInAutocompleteHistory) {
- FormData form_seen_by_ahm;
- EXPECT_CALL(single_field_fill_router(), OnWillSubmitForm(_, _, true))
- .WillOnce(SaveArg<0>(&form_seen_by_ahm));
-
- // Set up form.
- FormData form = test::GetFormData({.fields = {
- {.role = LOYALTY_MEMBERSHIP_ID},
- }});
- autofill_manager().AddSeenForm(form, {LOYALTY_MEMBERSHIP_ID});
- // Mark the loyalty card field as autofilled.
- test_api(autofill_manager())
- .FindCachedFormById(form.global_id())
- ->field(0)
- ->AddFieldModifier(FieldModifier::kAutofill);
- test_api(form).field(0).set_value(u"LOYALTYCARDNUMBER");
-
- FormSubmitted(form);
- ASSERT_EQ(form.fields().size(), form_seen_by_ahm.fields().size());
- EXPECT_FALSE(test_api(form_seen_by_ahm).field(0).should_autocomplete());
-}
-
// Regression test for crbug.com/428900385.
TEST_F(BrowserAutofillManagerTest, NullAutofillFieldDoesNotCrash) {
FormData form = test::GetFormData({.fields = {
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 66a8cd2..340dc93d 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
@@ -21,7 +21,9 @@
#include "base/time/time.h"
#include "base/version_info/version_info.h"
#include "build/build_config.h"
+#include "components/autofill/core/browser/form_structure_test_api.h"
#include "components/autofill/core/browser/foundations/test_autofill_client.h"
+#include "components/autofill/core/browser/test_utils/autofill_form_test_utils.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/autofill/core/browser/webdata/autocomplete/autocomplete_entry.h"
#include "components/autofill/core/browser/webdata/autofill_webdata_service.h"
@@ -160,6 +162,7 @@
EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
form.fields(),
+ /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -183,7 +186,7 @@
EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_));
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -204,7 +207,7 @@
EXPECT_CALL(*web_data_service_, AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -225,7 +228,7 @@
EXPECT_CALL(*web_data_service_, AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -247,7 +250,7 @@
EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_));
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -268,7 +271,7 @@
EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/false);
}
@@ -296,7 +299,7 @@
EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -322,7 +325,7 @@
EXPECT_CALL(*web_data_service_, AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -346,7 +349,7 @@
EXPECT_CALL(*web_data_service_, AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -372,7 +375,7 @@
EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_));
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
#endif
@@ -397,7 +400,7 @@
EXPECT_CALL(*web_data_service_, AddFormFields(_)).Times(0);
autocomplete_manager_->OnWillSubmitFormWithFields(
- form.fields(),
+ form.fields(), /*form=*/nullptr,
/*is_autocomplete_enabled=*/true);
}
@@ -468,7 +471,7 @@
// Simulate request for suggestions.
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
run_loop.Run();
@@ -497,7 +500,7 @@
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -525,7 +528,7 @@
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
// Simulate request for suggestions.
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
run_loop.Run();
@@ -550,7 +553,7 @@
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
// Simulate request for suggestions.
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
run_loop.Run();
@@ -574,7 +577,7 @@
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
// Simulate request for suggestions.
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
run_loop.Run();
@@ -611,7 +614,7 @@
// Simulate request for suggestions.
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -651,7 +654,7 @@
// Simulate request for suggestions.
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -684,7 +687,7 @@
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -716,7 +719,7 @@
EXPECT_CALL(mock_callback, Run(test_field_.global_id(), IsEmpty()))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -753,7 +756,7 @@
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -816,7 +819,7 @@
MockSuggestionsReturnedCallback mock_callback;
EXPECT_CALL(mock_callback, Run).Times(0);
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -827,7 +830,7 @@
u"SomePrefixTwo")))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
autocomplete_manager_->OnGetSingleFieldSuggestions(
- test_form_data_, /*form_structure=*/nullptr, test_field_,
+ test_form_data_, /*form=*/nullptr, test_field_,
/*trigger_autofill_field=*/nullptr, autofill_client_,
mock_callback.Get());
@@ -858,7 +861,7 @@
EXPECT_CALL(mock_callback, Run(test_field_.global_id(), testing::IsEmpty()))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
... (truncated)
Original Bug Report
Leakage of standalone CVC to Autocomplete history due to bypassed mitigations
Flapjack, 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 logic flaw in Chrome’s form parsing allows standalone CVC fields to bypass type classification if an email field is present. This bypasses security checks that prevent saving CVCs, resulting in the plaintext CVC being saved to the origin-agnostic Autocomplete history.
Affected files:
components/autofill/core/browser/form_parsing/form_field_parser.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.cccomponents/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A vulnerability in Chrome’s Autofill system potentially allows sensitive Credit Card Verification Codes (CVCs) to be stored in the origin-agnostic Autocomplete history. This occurs due to a combination of a heuristic skip during form parsing and incomplete security mitigations.
During form parsing, FormFieldParser::ParseFormFields runs multiple passes to identify field types. The ParseStandaloneCVCFields pass is specifically designed to identify standalone CVC fields (used for Virtual Card Number or card-on-file verification). However, this pass is conditionally skipped if an email field or other credit card fields have already been detected:
// form_field_parser.cc
bool found_email_field = !field_candidates.empty();
// ...
bool found_cc_fields = candidates_size != field_candidates.size();
if (!found_email_field && !found_cc_fields) {
ParseStandaloneCVCFields(context, fields, field_candidates);
}
If an email field is present (setting found_email_field = true), the standalone CVC field is not processed by this pass. Consequently, it is not classified as CREDIT_CARD_STANDALONE_VERIFICATION_CODE and defaults to UNKNOWN_TYPE.
Later, during form submission, BrowserAutofillManager::MaybeImportFromSubmittedForm checks if a field should be saved to Autocomplete history. It has a specific mitigation for CVCs:
// browser_autofill_manager.cc
if (autofill_field->Type().GetCreditCardType() == CREDIT_CARD_VERIFICATION_CODE) {
field.set_should_autocomplete(false);
}
Because the CVC field was classified as UNKNOWN_TYPE (or if it had been successfully classified as CREDIT_CARD_STANDALONE_VERIFICATION_CODE), this exact equality check fails. The should_autocomplete flag remains true.
The field is then evaluated by AutocompleteHistoryManager::IsFieldValueSaveable, which checks the field name against a regex blocklist via IsFieldNameMeaningfulForAutocomplete. This regex (^(?:(?:field|input|mat-input)[-_]?\d+|title|tan|mfa_text_box|pw|pin)$|^otp|otp$|\botp\b|cvc|cvn|cvv|captcha|passw|pass2|passcode|pwd|senha|pincode) correctly blocks names like cvc or pin, but it notably omits common CVC identifiers such as security_code or verification_code.
Since security_code is not blocklisted, the plaintext CVC value is saved to the origin-agnostic WebData Autocomplete database.
An attacker can exploit this by creating a malicious webpage with an input field named security_code. When the victim interacts with this field, AutocompleteSuggestionGenerator::GenerateSuggestions is invoked. While this function attempts to block suggestions for sensitive types (CREDIT_CARD_VERIFICATION_CODE or CREDIT_CARD_NUMBER), it does not block CREDIT_CARD_STANDALONE_VERIFICATION_CODE or UNKNOWN_TYPE. The saved CVC is presented as a suggestion, and if the user selects it, the attacker can extract the plaintext CVC.
Potential attacker steps
- A user visits a legitimate checkout page to verify a card-on-file. The form contains an email field (
<input name="email">) and a CVC field (<input name="security_code">). - Due to the presence of the email field, the CVC field is skipped during the standalone CVC pass and classified as
UNKNOWN_TYPE. - The user submits the form with their CVC. The
BrowserAutofillManagermitigation check fails because the type is not exactlyCREDIT_CARD_VERIFICATION_CODE. - The
AutocompleteHistoryManagerblocklist fails to catch the namesecurity_code. The plaintext CVC is saved to the origin-agnostic Autocomplete database. - An attacker hosts a malicious page with a form containing an input field named
security_code. - The victim visits the attacker’s page and focuses the input field. The field is parsed as
CREDIT_CARD_STANDALONE_VERIFICATION_CODE(since no email field is present). AutocompleteSuggestionGenerator::GenerateSuggestionsdoes not blockCREDIT_CARD_STANDALONE_VERIFICATION_CODE. It retrieves the saved CVC and displays it in the Autocomplete dropdown.- The victim selects the suggestion, filling the plaintext CVC into the attacker’s form, which is immediately exfiltrated.
Proposed Fix
- In
BrowserAutofillManager::MaybeImportFromSubmittedForm, expand the mitigation check to includeCREDIT_CARD_STANDALONE_VERIFICATION_CODE. - In
AutocompleteHistoryManager::IsFieldNameMeaningfulForAutocomplete, update the regex blocklist to include common CVC identifiers such assecurity_codeandverification_code, aligning it with the patterns used inlegacy_regex_patterns.json. - Re-evaluate the heuristic in
FormFieldParser::ParseFormFieldsthat skipsParseStandaloneCVCFieldswhen an email field is present, as checkout flows frequently require both.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.