CVE-2026-106342
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchcomponents/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc |
modified | |
ifcomponents/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc |
modified | |
forcomponents/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc |
modified | |
TEST_Fcomponents/autofill/content/browser/content_identity_credential_delegate_unittest.cc |
modified | |
BindLambdaForTestingcomponents/autofill/content/browser/content_identity_credential_delegate_unittest.cc |
modified |
Files Changed
components/autofill/content/browser/content_identity_credential_delegate_unittest.cccomponents/autofill/core/browser/suggestions/suggestion_generator.hcomponents/autofill/core/browser/autofill_type.cccomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cccomponents/autofill/content/browser/suggestions/identity_credential_suggestion_generator_unittest.cccomponents/autofill/core/browser/autofill_type_unittest.cccomponents/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.hcomponents/autofill/core/browser/foundations/browser_autofill_manager.cc
Patch
From 0408487a668e28fd0be682a5484cc407c1c417b8 Mon Sep 17 00:00:00 2001 From: Ryan Qin <[email protected]> Date: Wed, 09 Sep 2026 12:19:19 -0700 Subject: [PATCH] [FedCM] Restrict identity credential autofill to password fields The verified email code path is no longer supported in favor of Electronic Verification Platforms (EVP). The current implementation has security implications as described in the linked security bugs. This CL addresses them by restricting identity credential autofill suggestions exclusively to PASSWORD fields. A follow-up CL will clean up the remaining deprecated code paths and associated data structures. Fixed: 501805355, 512998592 Bug: 558682546 Change-Id: I931181d2f173b6439e5dbd46c0c078917645ffb4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8355384 Commit-Queue: Ryan Qin <[email protected]> Reviewed-by: Jihad Hanna <[email protected]> Cr-Commit-Position: refs/heads/main@{#1694803} --- diff --git a/components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc b/components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc index e481fe9..2cb4d6aa 100644 --- a/components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc +++ b/components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc @@ -23,82 +23,19 @@ namespace { -std::map<FieldType, std::u16string> CreateFederatedProfileFields( - IdentityRequestAccountPtr account) { - std::map<FieldType, std::u16string> fields; - - if (!account->email.empty() && - std::ranges::contains( - account->identity_provider->disclosure_fields, - content::IdentityRequestDialogDisclosureField::kEmail)) { - fields[EMAIL_ADDRESS] = base::UTF8ToUTF16(account->email); - } - - if (!account->name.empty() && - std::ranges::contains( - account->identity_provider->disclosure_fields, - content::IdentityRequestDialogDisclosureField::kName)) { - fields[NAME_FULL] = base::UTF8ToUTF16(account->name); - } - - if (!account->phone.empty() && - std::ranges::contains( - account->identity_provider->disclosure_fields, - content::IdentityRequestDialogDisclosureField::kPhoneNumber)) { - fields[PHONE_HOME_WHOLE_NUMBER] = base::UTF8ToUTF16(account->phone); - } - - return fields; -} - Suggestion CreateIdentityCredentialSuggestion( const IdentityCredential& credential, FieldType field_type) { - CHECK(field_type == PASSWORD || credential.fields.contains(field_type)); + CHECK_EQ(field_type, PASSWORD); Suggestion suggestion(SuggestionType::kIdentityCredential); suggestion.payload = Suggestion::IdentityCredentialPayload( credential.idp_config_url, credential.account_id, credential.fields); - - switch (field_type) { - case EMAIL_ADDRESS: - suggestion.main_text = - Suggestion::Text(credential.fields.at(EMAIL_ADDRESS)); - suggestion.icon = Suggestion::Icon::kEmail; - suggestion.minor_texts.emplace_back(l10n_util::GetStringFUTF16( - IDS_AUTOFILL_IDENTITY_CREDENTIAL_VERIFIED_MINOR_TEXT, - credential.idp_for_display)); - suggestion.labels.push_back({Suggestion::Text(l10n_util::GetStringUTF16( - IDS_AUTOFILL_IDENTITY_CREDENTIAL_VERIFIED_EMAIL_LABEL))}); - break; - case PASSWORD: - suggestion.main_text = Suggestion::Text(credential.main_text); - suggestion.custom_icon = credential.custom_icon; - // TODO(crbug.com/410421491): support more context. - suggestion.labels.push_back({Suggestion::Text(l10n_util::GetStringFUTF16( - IDS_AUTOFILL_IDENTITY_CREDENTIAL_LABEL_TEXT, - credential.idp_for_display))}); - break; - case NAME_FULL: - case PHONE_HOME_WHOLE_NUMBER: - suggestion.main_text = Suggestion::Text(credential.fields.at(field_type)); - suggestion.icon = Suggestion::Icon::kAccount; - suggestion.minor_texts.emplace_back(l10n_util::GetStringFUTF16( - IDS_AUTOFILL_IDENTITY_CREDENTIAL_PROVIDED_MINOR_TEXT, - credential.idp_for_display)); - break; - case NAME_FIRST: - case UNKNOWN_TYPE: - // We should not reach this case since an early return in - // `IdentityCredentialSuggestionGenerator::FetchSuggestionData()` should - // prevent this. - // TODO(crbug.com/380367784): Add support for suggestions on NAME_FIRST - // fields. - default: - // The given `field_type` must be one of the IdentityCredential types - // in the co-domain of AutofillType::GetIdentityCredentialType(). - NOTREACHED(); - } - + suggestion.main_text = Suggestion::Text(credential.main_text); + suggestion.custom_icon = credential.custom_icon; + // TODO(crbug.com/410421491): support more context. + suggestion.labels.push_back({Suggestion::Text( + l10n_util::GetStringFUTF16(IDS_AUTOFILL_IDENTITY_CREDENTIAL_LABEL_TEXT, + credential.idp_for_display))}); return suggestion; } @@ -139,9 +76,8 @@ FieldType trigger_field_type = trigger_autofill_field->Type().GetIdentityCredentialType(); - // TODO(crbug.com/380367784): Add support for suggestions on NAME_FIRST - // fields. - if (trigger_field_type == UNKNOWN_TYPE || trigger_field_type == NAME_FIRST) { + // Identity credential suggestions are only supported on PASSWORD fields. + if (trigger_field_type != PASSWORD) { callback({SuggestionDataSource::kIdentityCredential, {}}); return; } @@ -152,8 +88,6 @@ return; } - // TODO(crbug.com/380367784): reproduce and add a test to make sure this - // works properly when FedCM is called from inner frames. content::webid::AutofillSource* source = source_.Run(); if (!source) { @@ -170,25 +104,18 @@ std::vector<IdentityCredential> credentials; for (IdentityRequestAccountPtr account : *accounts) { - bool delegated = - account->identity_provider->format && - *account->identity_provider->format == blink::mojom::Format::kSdJwt; bool is_returning_credential = account->idp_claimed_login_state.value_or( account->browser_trusted_login_state) == content::IdentityRequestAccount::LoginState::kSignIn; - if (!delegated && !is_returning_credential) { + if (!is_returning_credential) { continue; } - if (IdentityCredential credential( - account->identity_provider->idp_metadata.config_url, account->id, - base::UTF8ToUTF16(account->identity_provider->idp_for_display), - base::UTF8ToUTF16(account->email), - CreateFederatedProfileFields(account), account->decoded_picture); - trigger_field_type == PASSWORD || - credential.fields.contains(trigger_field_type)) { - credentials.push_back(std::move(credential)); - } + credentials.emplace_back( + account->identity_provider->idp_metadata.config_url, account->id, + base::UTF8ToUTF16(account->identity_provider->idp_for_display), + base::UTF8ToUTF16(account->email), + std::map<FieldType, std::u16string>(), account->decoded_picture); } callback( diff --git a/components/autofill/core/browser/autofill_type.cc b/components/autofill/core/browser/autofill_type.cc index b95aec8c..09a1141 100644 --- a/components/autofill/core/browser/autofill_type.cc +++ b/components/autofill/core/browser/autofill_type.cc @@ -77,8 +77,7 @@ // more acute if the set below grows further (e.g., to include USERNAME) and the // overlap with PWM types grows (the Autofill server may predict // EMAIL_ADDRESS and USERNAME for the same field). -constexpr FieldTypeSet kIdentityCredentialFieldTypes = { - NAME_FIRST, NAME_FULL, EMAIL_ADDRESS, PHONE_HOME_WHOLE_NUMBER, PASSWORD}; +constexpr FieldTypeSet kIdentityCredentialFieldTypes = {PASSWORD}; // TODO(crbug.com/432645177): Remove `EMAIL_OR_LOYALTY_MEMBERSHIP_ID` and // represent it as union of the other three types. That means the getter diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager.cc b/components/autofill/core/browser/foundations/browser_autofill_manager.cc index 724761a..56046d0 100644 --- a/components/autofill/core/browser/foundations/browser_autofill_manager.cc +++ b/components/autofill/core/browser/foundations/browser_autofill_manager.cc @@ -1357,8 +1357,6 @@ std::vector<Suggestion> address_suggestions = extract_vector(FillingProduct::kAddress); - std::vector<Suggestion> identity_credentials_suggestions = - extract_vector(FillingProduct::kIdentityCredential); std::vector<Suggestion> loyalty_card_suggestions = extract_vector(FillingProduct::kLoyaltyCard); std::vector<Suggestion> autocomplete_suggestions = @@ -1374,11 +1372,6 @@ std::move(loyalty_card_suggestions)); } - if (!identity_credentials_suggestions.empty()) { - MergeIdentityCredentialsAndAddressSuggestions( - address_suggestions, std::move(identity_credentials_suggestions)); - } - if (!autocomplete_suggestions.empty() && trigger_field) { MergeAutocompleteAndAddressSuggestions( address_suggestions, std::move(autocomplete_suggestions), @@ -1387,17 +1380,6 @@ return address_suggestions; } -void BrowserAutofillManager::MergeIdentityCredentialsAndAddressSuggestions( - std::vector<Suggestion>& suggestions, - std::vector<Suggestion> identity_credential_suggestions) { - // TODO(crbug.com/380367784): figure out what to do when both verified - // and unverified suggestions point to the same email address. - suggestions.insert( - suggestions.begin(), - std::make_move_iterator(identity_credential_suggestions.begin()), - std::make_move_iterator(identity_credential_suggestions.end())); -} - void BrowserAutofillManager::MergeAutocompleteAndAddressSuggestions( std::vector<Suggestion>& suggestions, std::vector<Suggestion> autocomplete_suggestions, @@ -3226,21 +3208,6 @@ break; } - if (const IdentityCredentialDelegate* identity_credential_delegate = - client().GetIdentityCredentialDelegate()) { - // Only <input autocomplete="email webidentity"> fields are considered. - if (std::optional<AutocompleteParsingResult> autocomplete = - ParseAutocompleteAttribute(autofill_field.autocomplete_attribute()); - autocomplete && autocomplete->webidentity) { - std::vector<Suggestion> verified_suggestions = - identity_credential_delegate->GetVerifiedAutofillSuggestions( - form, &form_structure, field, &autofill_field, client()); - // Insert verified suggestions above unverified ones. - MergeIdentityCredentialsAndAddressSuggestions( - suggestions, std::move(verified_suggestions)); - } - } - return suggestions; } diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager.h b/components/autofill/core/browser/foundations/browser_autofill_manager.h index e827bd4..978cafb 100644 --- a/components/autofill/core/browser/foundations/browser_autofill_manager.h +++ b/components/autofill/core/browser/foundations/browser_autofill_manager.h @@ -545,11 +545,6 @@ const AutofillField* trigger_field, AutofillSuggestionTriggerSource trigger_source); - // Combines identity credential suggestions and existing suggestions into a - // single list, prioritizing identity credential suggestions first. - static void MergeIdentityCredentialsAndAddressSuggestions( - std::vector<Suggestion>& suggestions, - std::vector<Suggestion> identity_credential_suggestions); // Combines autocomplete suggestions and existing suggestions into a // single list, prioritizing address suggestions and filtering out diff --git a/components/autofill/core/browser/suggestions/suggestion_generator.h b/components/autofill/core/browser/suggestions/suggestion_generator.h index 32a78f6..8cf6d3c0 100644 --- a/components/autofill/core/browser/suggestions/suggestion_generator.h +++ b/components/autofill/core/browser/suggestions/suggestion_generator.h @@ -93,13 +93,11 @@ {SuggestionDataSource::kOneTimePassword, {SuggestionDataSource::kPasskey}}, // Address can merge with: - // - Verified Identity: To show verified details alongside profiles. // - Loyalty Cards: To extend email suggestions. // - Autocomplete: To complement non-stored email addresses. // - Passkeys: Every SuggestionDataSource can be merged with passkeys. {SuggestionDataSource::kAddress, - {SuggestionDataSource::kIdentityCredential, - SuggestionDataSource::kLoyaltyCard, SuggestionDataSource::kPasskey, + {SuggestionDataSource::kLoyaltyCard, SuggestionDataSource::kPasskey, SuggestionDataSource::kAutocomplete}}, {SuggestionDataSource::kIdentityCredential, {SuggestionDataSource::kPasskey}}, diff --git a/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc b/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc index f1881937..61448f1 100644 --- a/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc +++ b/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc @@ -116,7 +116,7 @@ EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(std::nullopt)); - test_api(form()).SetFieldTypes({EMAIL_ADDRESS}); + test_api(form()).SetFieldTypes({PASSWORD}); std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions( form().ToFormData(), &form(), field(), &field(), client()); ASSERT_EQ(0ul, suggestions.size()); @@ -135,13 +135,14 @@ EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts)); - test_api(form()).SetFieldTypes({EMAIL_ADDRESS}); + test_api(form()).SetFieldTypes({PASSWORD}); std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions( form().ToFormData(), &form(), field(), &field(), client()); ASSERT_EQ(0ul, suggestions.size()); } -TEST_F(ContentIdentityCredentialDelegateTest, UnsupportedFieldType) { +TEST_F(ContentIdentityCredentialDelegateTest, + NoSuggestionsForUnsupportedFields) { MockAutofillSource mock; ContentIdentityCredentialDelegate delegate( @@ -152,204 +153,17 @@ EXPECT_CALL(mock, GetAutofillSuggestions).Times(0); - test_api(form()).SetFieldTypes({UNKNOWN_TYPE}); - std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions( - form().ToFormData(), &form(), field(), &field(), client()); - ASSERT_EQ(0ul, suggestions.size()); -} - -TEST_F(ContentIdentityCredentialDelegateTest, GetVerifiedEmailRequest) { - MockAutofillSource mock; - - ContentIdentityCredentialDelegate delegate( - base::BindLambdaForTesting([&mock]() { - content::webid::AutofillSource* result = &mock; - return result; - })); - - IdentityRequestAccountPtr account = CreateTestAccount(); - // The delegated flow requires an IdP with a specific format. - account->identity_provider->format = blink::mojom::Format::kSdJwt; - // Use only "email" in the selective disclosure request. - account->identity_provider->disclosure_fields = { - content::IdentityRequestDialogDisclosureField::kEmail}; - std::vector<IdentityRequestAccountPtr> accounts = {account}; - - EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts)); - - test_api(form()).SetFieldTypes({EMAIL_ADDRESS}); - std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions( - form().ToFormData(), &form(), field(), &field(), client()); - ASSERT_EQ(1ul, suggestions.size()); - - Suggestion suggestion = suggestions[0]; - EXPECT_EQ(suggestion.main_text.value, u"[email protected]"); - ASSERT_EQ(suggestion.labels.size(), 1ul); - ASSERT_EQ(suggestion.minor_texts.size(), 1ul); - EXPECT_EQ(suggestion.icon, Suggestion::Icon::kEmail); - - // Expect the payload to be populated properly. - Suggestion::IdentityCredentialPayload payload = - suggestion.GetPayload<Suggestion::IdentityCredentialPayload>(); - EXPECT_EQ(payload.account_id, "id"); - EXPECT_EQ(payload.config_url, GURL("https://idp.example")); - - // Expect only one field to be available in the payload. - ASSERT_EQ(payload.fields.size(), 1ul); - - // Expect that email is previewed/filled because it was requested in the - // conditional request. - ASSERT_TRUE(payload.fields.contains(EMAIL_ADDRESS)); - EXPECT_EQ(payload.fields[EMAIL_ADDRESS], u"[email protected]"); - - // Expect that name isn't previewed/filled because it wasn't requested in the - // conditional request. - EXPECT_FALSE(payload.fields.contains(NAME_FULL)); -} - -TEST_F(ContentIdentityCredentialDelegateTest, SuggestPhoneNumbers) { - MockAutofillSource mock; - - ContentIdentityCredentialDelegate delegate( - base::BindLambdaForTesting([&mock]() { - content::webid::AutofillSource* result = &mock; - return result; - })); - - IdentityRequestAccountPtr account = CreateTestAccount(); - // The delegated flow requires an IdP with a specific format. - account->identity_provider->format = blink::mojom::Format::kSdJwt; - // Use "email" AND "phone-number" in the selective disclosure request. - account->identity_provider->disclosure_fields = { - content::IdentityRequestDialogDisclosureField::kPhoneNumber, - content::IdentityRequestDialogDisclosureField::kEmail}; - std::vector<IdentityRequestAccountPtr> accounts = {account}; - - EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts)); - - test_api(form()).SetFieldTypes({PHONE_HOME_WHOLE_NUMBER}); - std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions( - form().ToFormData(), &form(), field(), &field(), client()); - ASSERT_EQ(1ul, suggestions.size()); - - Suggestion suggestion = suggestions[0]; - EXPECT_EQ(suggestion.main_text.value, u"+1 (234) 567-8910"); ... (file diff truncated)
Regression Test / PoC
diff --git a/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc b/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc
index f1881937..61448f1 100644
--- a/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc
+++ b/components/autofill/content/browser/content_identity_credential_delegate_unittest.cc
@@ -116,7 +116,7 @@
EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(std::nullopt));
- test_api(form()).SetFieldTypes({EMAIL_ADDRESS});
+ test_api(form()).SetFieldTypes({PASSWORD});
std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
form().ToFormData(), &form(), field(), &field(), client());
ASSERT_EQ(0ul, suggestions.size());
@@ -135,13 +135,14 @@
EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts));
- test_api(form()).SetFieldTypes({EMAIL_ADDRESS});
+ test_api(form()).SetFieldTypes({PASSWORD});
std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
form().ToFormData(), &form(), field(), &field(), client());
ASSERT_EQ(0ul, suggestions.size());
}
-TEST_F(ContentIdentityCredentialDelegateTest, UnsupportedFieldType) {
+TEST_F(ContentIdentityCredentialDelegateTest,
+ NoSuggestionsForUnsupportedFields) {
MockAutofillSource mock;
ContentIdentityCredentialDelegate delegate(
@@ -152,204 +153,17 @@
EXPECT_CALL(mock, GetAutofillSuggestions).Times(0);
- test_api(form()).SetFieldTypes({UNKNOWN_TYPE});
- std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
- form().ToFormData(), &form(), field(), &field(), client());
- ASSERT_EQ(0ul, suggestions.size());
-}
-
-TEST_F(ContentIdentityCredentialDelegateTest, GetVerifiedEmailRequest) {
- MockAutofillSource mock;
-
- ContentIdentityCredentialDelegate delegate(
- base::BindLambdaForTesting([&mock]() {
- content::webid::AutofillSource* result = &mock;
- return result;
- }));
-
- IdentityRequestAccountPtr account = CreateTestAccount();
- // The delegated flow requires an IdP with a specific format.
- account->identity_provider->format = blink::mojom::Format::kSdJwt;
- // Use only "email" in the selective disclosure request.
- account->identity_provider->disclosure_fields = {
- content::IdentityRequestDialogDisclosureField::kEmail};
- std::vector<IdentityRequestAccountPtr> accounts = {account};
-
- EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts));
-
- test_api(form()).SetFieldTypes({EMAIL_ADDRESS});
- std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
- form().ToFormData(), &form(), field(), &field(), client());
- ASSERT_EQ(1ul, suggestions.size());
-
- Suggestion suggestion = suggestions[0];
- EXPECT_EQ(suggestion.main_text.value, u"[email protected]");
- ASSERT_EQ(suggestion.labels.size(), 1ul);
- ASSERT_EQ(suggestion.minor_texts.size(), 1ul);
- EXPECT_EQ(suggestion.icon, Suggestion::Icon::kEmail);
-
- // Expect the payload to be populated properly.
- Suggestion::IdentityCredentialPayload payload =
- suggestion.GetPayload<Suggestion::IdentityCredentialPayload>();
- EXPECT_EQ(payload.account_id, "id");
- EXPECT_EQ(payload.config_url, GURL("https://idp.example"));
-
- // Expect only one field to be available in the payload.
- ASSERT_EQ(payload.fields.size(), 1ul);
-
- // Expect that email is previewed/filled because it was requested in the
- // conditional request.
- ASSERT_TRUE(payload.fields.contains(EMAIL_ADDRESS));
- EXPECT_EQ(payload.fields[EMAIL_ADDRESS], u"[email protected]");
-
- // Expect that name isn't previewed/filled because it wasn't requested in the
- // conditional request.
- EXPECT_FALSE(payload.fields.contains(NAME_FULL));
-}
-
-TEST_F(ContentIdentityCredentialDelegateTest, SuggestPhoneNumbers) {
- MockAutofillSource mock;
-
- ContentIdentityCredentialDelegate delegate(
- base::BindLambdaForTesting([&mock]() {
- content::webid::AutofillSource* result = &mock;
- return result;
- }));
-
- IdentityRequestAccountPtr account = CreateTestAccount();
- // The delegated flow requires an IdP with a specific format.
- account->identity_provider->format = blink::mojom::Format::kSdJwt;
- // Use "email" AND "phone-number" in the selective disclosure request.
- account->identity_provider->disclosure_fields = {
- content::IdentityRequestDialogDisclosureField::kPhoneNumber,
- content::IdentityRequestDialogDisclosureField::kEmail};
- std::vector<IdentityRequestAccountPtr> accounts = {account};
-
- EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts));
-
- test_api(form()).SetFieldTypes({PHONE_HOME_WHOLE_NUMBER});
- std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
- form().ToFormData(), &form(), field(), &field(), client());
- ASSERT_EQ(1ul, suggestions.size());
-
- Suggestion suggestion = suggestions[0];
- EXPECT_EQ(suggestion.main_text.value, u"+1 (234) 567-8910");
- ASSERT_EQ(suggestion.minor_texts.size(), 1ul);
-
- // Expect the payload to be populated properly.
- Suggestion::IdentityCredentialPayload payload =
- suggestion.GetPayload<Suggestion::IdentityCredentialPayload>();
- EXPECT_EQ(payload.account_id, "id");
- EXPECT_EQ(payload.config_url, GURL("https://idp.example"));
-
- // Expect two fields to be available in the payload: emails and usernames.
- ASSERT_EQ(payload.fields.size(), 2ul);
-
- // Expect that email is previewed/filled because it was requested in the
- // conditional request.
- ASSERT_TRUE(payload.fields.contains(EMAIL_ADDRESS));
- EXPECT_EQ(payload.fields[EMAIL_ADDRESS], u"[email protected]");
-
- // Expect that email is previewed/filled because it was requested in the
- // conditional request.
- ASSERT_TRUE(payload.fields.contains(PHONE_HOME_WHOLE_NUMBER));
- EXPECT_EQ(payload.fields[PHONE_HOME_WHOLE_NUMBER], u"+1 (234) 567-8910");
-}
-
-TEST_F(ContentIdentityCredentialDelegateTest,
- GetSuggestionForFieldThatWasntRequested) {
- MockAutofillSource mock;
-
- ContentIdentityCredentialDelegate delegate(
- base::BindLambdaForTesting([&mock]() {
- content::webid::AutofillSource* result = &mock;
- return result;
- }));
-
- IdentityRequestAccountPtr account = CreateTestAccount();
- // The delegated flow requires an IdP with a specific format.
- account->identity_provider->format = blink::mojom::Format::kSdJwt;
- // Use only "email" in the selective disclosure request.
- account->identity_provider->disclosure_fields = {
- content::IdentityRequestDialogDisclosureField::kEmail};
- std::vector<IdentityRequestAccountPtr> accounts = {account};
-
- EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts));
-
- test_api(form()).SetFieldTypes({NAME_FULL});
- std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
- form().ToFormData(), &form(), field(), &field(), client());
- ASSERT_EQ(0ul, suggestions.size());
-}
-
-TEST_F(ContentIdentityCredentialDelegateTest,
- GetSuggestionForFieldThatRequestedButIsUnavailable) {
- MockAutofillSource mock;
-
- ContentIdentityCredentialDelegate delegate(
- base::BindLambdaForTesting([&mock]() {
- content::webid::AutofillSource* result = &mock;
- return result;
- }));
-
- IdentityRequestAccountPtr account = CreateTestAccount();
-
- // The delegated flow requires an IdP with a specific format.
- account->identity_provider->format = blink::mojom::Format::kSdJwt;
-
- // Set email to an unavailable string.
- account->email = "";
-
- // Use only "email" in the selective disclosure request.
- account->identity_provider->disclosure_fields = {
- content::IdentityRequestDialogDisclosureField::kEmail};
- std::vector<IdentityRequestAccountPtr> accounts = {account};
-
- EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts));
-
- test_api(form()).SetFieldTypes({EMAIL_ADDRESS});
- std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
- form().ToFormData(), &form(), field(), &field(), client());
- ASSERT_EQ(0ul, suggestions.size());
-}
-
-TEST_F(ContentIdentityCredentialDelegateTest,
- GetSuggestionsForDelegatedCredentialAvailableForSignUp) {
- MockAutofillSource mock;
-
- ContentIdentityCredentialDelegate delegate(
- base::BindLambdaForTesting([&mock]() {
- content::webid::AutofillSource* result = &mock;
- return result;
- }));
-
- IdentityRequestAccountPtr account = CreateTestAccount();
- // The delegated flow requires an IdP with a specific format.
- account->identity_provider->format = blink::mojom::Format::kSdJwt;
- account->idp_claimed_login_state =
- content::IdentityRequestAccount::LoginState::kSignUp;
- std::vector<IdentityRequestAccountPtr> accounts = {account};
-
- EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts));
-
- test_api(form()).SetFieldTypes({PASSWORD});
- std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
- form().ToFormData(), &form(), field(), &field(), client());
- ASSERT_EQ(1ul, suggestions.size());
-
- Suggestion suggestion = suggestions[0];
- EXPECT_EQ(suggestion.main_text.value, u"[email protected]");
- EXPECT_EQ(suggestion.labels.size(), 1ul);
- EXPECT_EQ(suggestion.minor_texts.size(), 0ul);
-
- // Expect the payload to be populated properly.
- Suggestion::IdentityCredentialPayload payload =
- suggestion.GetPayload<Suggestion::IdentityCredentialPayload>();
- EXPECT_EQ(payload.account_id, "id");
- EXPECT_EQ(payload.config_url, GURL("https://idp.example"));
-
- // Expect no field to be available in the payload for PASSWORD.
- EXPECT_TRUE(payload.fields.empty());
+ for (FieldType unsupported_type :
+ {UNKNOWN_TYPE, EMAIL_ADDRESS, PHONE_HOME_WHOLE_NUMBER, NAME_FULL,
+ NAME_FIRST, USERNAME_AND_EMAIL_ADDRESS, CREDIT_CARD_NUMBER}) {
+ SCOPED_TRACE(testing::Message() << "Testing FieldType: "
+ << FieldTypeToStringView(unsupported_type));
+ test_api(form()).SetFieldTypes({unsupported_type});
+ std::vector<Suggestion> suggestions =
+ delegate.GetVerifiedAutofillSuggestions(form().ToFormData(), &form(),
+ field(), &field(), client());
+ EXPECT_TRUE(suggestions.empty());
+ }
}
TEST_F(ContentIdentityCredentialDelegateTest,
@@ -412,51 +226,6 @@
EXPECT_TRUE(payload.fields.empty());
}
-TEST_F(ContentIdentityCredentialDelegateTest, GetProvidedNameRequest) {
- MockAutofillSource mock;
-
- ContentIdentityCredentialDelegate delegate(
- base::BindLambdaForTesting([&mock]() {
- content::webid::AutofillSource* result = &mock;
- return result;
- }));
-
- IdentityRequestAccountPtr account = CreateTestAccount();
- // The delegated flow requires an IdP with a specific format.
- account->identity_provider->format = blink::mojom::Format::kSdJwt;
- // Use only "name" in the selective disclosure request.
- account->identity_provider->disclosure_fields = {
- content::IdentityRequestDialogDisclosureField::kName};
- std::vector<IdentityRequestAccountPtr> accounts = {account};
-
- EXPECT_CALL(mock, GetAutofillSuggestions).WillOnce(Return(accounts));
-
- test_api(form()).SetFieldTypes({NAME_FULL});
- std::vector<Suggestion> suggestions = delegate.GetVerifiedAutofillSuggestions(
- form().ToFormData(), &form(), field(), &field(), client());
- ASSERT_EQ(1ul, suggestions.size());
-
- Suggestion suggestion = suggestions[0];
- EXPECT_EQ(suggestion.main_text.value, u"John");
- EXPECT_EQ(suggestion.labels.size(), 0ul);
- EXPECT_EQ(suggestion.minor_texts.size(), 1ul);
- EXPECT_EQ(suggestion.icon, Suggestion::Icon::kAccount);
-
- // Expect the payload to be populated properly.
- Suggestion::IdentityCredentialPayload payload =
- suggestion.GetPayload<Suggestion::IdentityCredentialPayload>();
- EXPECT_EQ(payload.account_id, "id");
- EXPECT_EQ(payload.config_url, GURL("https://idp.example"));
-
- // Expect only one field to be available in the payload.
- ASSERT_EQ(payload.fields.size(), 1ul);
-
- // Expect that email is previewed/filled because it was requested in the
- // conditional request.
- ASSERT_TRUE(payload.fields.contains(NAME_FULL));
- EXPECT_EQ(payload.fields[NAME_FULL], u"John");
-}
-
} // namespace
} // namespace autofill
diff --git a/components/autofill/content/browser/suggestions/identity_credential_suggestion_generator_unittest.cc b/components/autofill/content/browser/suggestions/identity_credential_suggestion_generator_unittest.cc
index 704bf95..43b9e66 100644
... (truncated)
Original Bug Report
FedCM: Potential user PII disclosure to RP renderer during autofill preview
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A privacy vulnerability in FedCM conditional autofill allows a Relying Party (RP) renderer to obtain sensitive user PII (email, name, phone) before user consent is granted. When a user highlights a FedCM suggestion, the browser sends the raw PII to the renderer to display an autofill preview, allowing a compromised renderer to harvest this data.
Affected files:
components/autofill/core/browser/ui/autofill_external_delegate.cccomponents/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cccomponents/autofill/core/browser/filling/form_filler.cccomponents/autofill/content/renderer/autofill_agent.ccthird_party/blink/renderer/core/html/forms/text_control_element.cc
Estimated timestamp from git blame: 2025-03-24
Summary
A potential privacy vulnerability has been identified in the Federated Credential Management (FedCM) conditional autofill implementation. When FedCM suggestions are displayed in the autofill popup, hovering over or navigating to a suggestion triggers a ‘preview’ action. This action sends sensitive user information (PII) from the browser process to the Relying Party (RP) renderer process before the user has provided explicit consent via the FedCM UI.
Vulnerability Details
In components/autofill/core/browser/ui/autofill_external_delegate.cc, the method DidSelectSuggestion handles the event when a user highlights a suggestion (e.g., via mouse hover or arrow keys). For suggestions of type SuggestionType::kIdentityCredential, the code directly proceeds to a preview fill:
// components/autofill/core/browser/ui/autofill_external_delegate.cc
case SuggestionType::kIdentityCredential: {
VerifiedProfile profile =
suggestion.GetPayload<Suggestion::IdentityCredentialPayload>().fields;
manager_->FillOrPreviewForm(mojom::ActionPersistence::kPreview,
query_form_, query_field_.global_id(),
&profile, GetTriggerSource(),
/*blocked_fields=*/{});
break;
}
Unlike the ‘Accept’ path (triggered by a click), which correctly gates the disclosure behind the FedCM modal/consent dialog, the ‘Preview’ path has no such protection.
The data flow is as follows:
- The browser’s
FormFillerretrieves raw PII strings (email, name, etc.) from theVerifiedProfilepayload. - The browser sends a
mojom::AutofillAgent::ApplyFieldsActionIPC to the renderer, containing the un-obfuscated PII. - In the renderer,
AutofillAgentandform_utilreceive this data and callblink::TextControlElement::SetSuggestedValue, which stores the PII in thesuggested_value_member variable of the target form element.
Impact
A compromised RP renderer process can directly access the suggested_value_ of form elements to harvest the user’s private data (email, full name, phone number) without the user ever clicking ‘Accept’ or ‘Continue’. This violates the core privacy principle of FedCM: that no user information should be disclosed to the Relying Party until the user explicitly approves the disclosure through the browser’s trusted UI.
Note: This feature is currently gated behind the FedCmAutofill flag and is not yet enabled by default in stable releases.
Potential Reproduction Steps
Note: These steps are based on code analysis and have not been validated with a running proof-of-concept.
- Enable the
FedCmAutofillfeature flag. - Navigate to a site that has initiated a conditional FedCM request (
mediation: 'conditional'). - Ensure the user is logged into an Identity Provider that returns account details.
- Focus an input field to trigger the autofill popup.
- Hover the mouse over an IdP account suggestion.
- In a debugger attached to the renderer process, verify that
blink::TextControlElement::suggested_value_is populated with the user’s PII.
Suggested Fix
The browser should not send un-obfuscated PII to the renderer during the preview phase for kIdentityCredential suggestions. Instead, the preview should either be disabled for this type until consent is granted, or the browser should send a masked value (e.g., ‘••••@example.com’) to the renderer for the visual preview.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.