CVE-2026-17880
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 <[email protected]> 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 <[email protected]> Reviewed-by: Jihad Hanna <[email protected]> Commit-Queue: Gianmarco Picarella <[email protected]> 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
Priority inversion in TravelFieldParser allows cross-origin CVC leakage via Autocomplete
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 Autofill heuristics allows sensitive Credit Card Verification Codes (CVC) to be misclassified as UNKNOWN_TYPE due to a priority inversion where the Travel parser’s score (1.2) overrides the Credit Card parser’s score (1.0). This misclassification bypasses explicit security checks meant to block CVC storage, resulting in the CVC being saved to the cross-origin Autocomplete history where malicious sites can retrieve it.
Affected files:
components/autofill/core/browser/form_parsing/travel_field_parser.cccomponents/autofill/core/browser/form_parsing/form_field_parser.hcomponents/autofill/core/browser/foundations/browser_autofill_manager.cccomponents/autofill/core/browser/single_field_fillers/autocomplete/autocomplete_history_manager.cc
Estimated timestamp from git blame: 2019-02-08
Summary
A potential vulnerability in Chrome’s Autofill system allows sensitive Credit Card Verification Codes (CVC) to bypass security checks and be saved in cleartext to the origin-agnostic Autocomplete database. This occurs due to a priority inversion where the TravelFieldParser assigns an UNKNOWN_TYPE classification with a higher score than the specific CREDIT_CARD_VERIFICATION_CODE classification assigned by the CreditCardFieldParser or StandaloneCvcFieldParser. Once saved, any site can retrieve the sensitive data by using a field with the same name.
Technical Details
When Chrome parses a form to determine field types, it uses multiple local heuristics. The process is orchestrated by FormFieldParser::ParseFormFields (components/autofill/core/browser/form_parsing/form_field_parser.cc).
- Priority Inversion: If a form contains an input named
flight_verification, it matches patterns for two different parsers:TravelFieldParser::Parsematches theFLIGHTregex (airline|flightinlegacy_regex_patterns.json:3242). It assigns the candidateUNKNOWN_TYPEwith a base score of1.2f(kBaseTravelParserScoredefined inform_field_parser.h:252).CreditCardFieldParser::Parse(orStandaloneCvcFieldParser) matches theCREDIT_CARD_VERIFICATION_CODEregex, which explicitly includes the substringverification(legacy_regex_patterns.json:2783). It assigns the candidateCREDIT_CARD_VERIFICATION_CODEwith a base score of1.0f(kBaseCreditCardParserScoredefined inform_field_parser.h:254).- During candidate resolution (
FieldCandidates::BestHeuristicType()infield_candidates.cc:34-47), the candidate with the highest score wins. Because1.2f>1.0f, the field’sbest_heuristic_typeis incorrectly set toUNKNOWN_TYPE.
- Security Check Bypass:
BrowserAutofillManager::MaybeImportFromSubmittedForm(browser_autofill_manager.cc:587) processes fields prior to saving them to Autocomplete. It contains a specific security check to block CVCs:if (autofill_field->Type().GetCreditCardType() == CREDIT_CARD_VERIFICATION_CODE) { field.set_should_autocomplete(false); }(browser_autofill_manager.cc:610-615). Because the heuristic classification was overridden toUNKNOWN_TYPE, this check evaluates tofalse, andshould_autocompleteremainstrue. - Blocklist Gap: The field is forwarded to
AutocompleteHistoryManager::OnWillSubmitFormWithFields, which validates the field name usingIsFieldNameMeaningfulForAutocomplete(autocomplete_history_manager.cc:155-166). The blocklist regex (kRegex) forbids names containingcvc,cvn,cvv,passw,pwd, orpin, but completely lacks terms used by the CVC heuristics, likeverificationorsecurity code. Thus,flight_verificationis deemed a valid, saveable name.
The sensitive CVC is subsequently saved to the local Web Data SQLite database, associated with the origin-agnostic name flight_verification.
Suggested Attacker Steps
Note: These are potential steps as our tooling agent cannot execute code to verify the exploit end-to-end.
- An attacker compromises a benign airline checkout page (or creates a seemingly benign one) and ensures the CVC input field is named
<input type="text" name="flight_verification">. - A victim enters their CVC and submits the form. Due to the parser conflict, the CVC is saved to their local Autocomplete database.
- The attacker hosts a separate, cross-origin malicious site containing the same field:
<input type="text" name="flight_verification">. - When the victim visits the malicious site and clicks anywhere on the page (or interacts with a decoy field), Chrome offers the saved CVC as an Autocomplete suggestion.
- If the victim accepts the suggestion, the CVC is filled, and the attacker’s JavaScript immediately exfiltrates it.
Suggested Remediation
- Adjust Parser Scoring: Modify
TravelFieldParser(or candidate resolution logic) so that generic classifications likeUNKNOWN_TYPEdo not override specific, sensitive classifications likeCREDIT_CARD_VERIFICATION_CODEfrom other parsers. - Expand Autocomplete Blocklist: Update the regex
kRegexinAutocompleteHistoryManager::IsFieldNameMeaningfulForAutocompleteto include missing sensitive keywords used by the CVC parsers, such asverificationandsecurity.?code.
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.