Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Autofill
DescriptionMissing authorization in Autofill
ComponentAutofill
Sub-componentFedCM
Bug ClassAuthorization Error
Tracker501805355
Fix commit0408487a668e (chromium/src) +73/-481
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
switch
components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc
modified
if
components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc
modified
for
components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc
modified
TEST_F
components/autofill/content/browser/content_identity_credential_delegate_unittest.cc
modified
BindLambdaForTesting
components/autofill/content/browser/content_identity_credential_delegate_unittest.cc
modified

Files Changed

  • components/autofill/content/browser/content_identity_credential_delegate_unittest.cc
  • components/autofill/core/browser/suggestions/suggestion_generator.h
  • components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
  • components/autofill/core/browser/autofill_type.cc
  • components/autofill/content/browser/suggestions/identity_credential_suggestion_generator_unittest.cc
  • components/autofill/core/browser/autofill_type_unittest.cc
  • components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc
  • components/autofill/core/browser/foundations/browser_autofill_manager.h
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc
From 0408487a668e28fd0be682a5484cc407c1c417b8 Mon Sep 17 00:00:00 2001
From: Ryan Qin <ryqin@google.com>
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 <ryqin@chromium.org>
Reviewed-by: Jihad Hanna <jihadghanna@google.com>
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"john@email.com");
-  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"john@email.com");
-
-  // 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)
Loading diff…

Regression Test / PoC

shipped with the fix
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"john@email.com");
-  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"john@email.com");
-
-  // 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"john@email.com");
-
-  // 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"john@email.com");
-  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)
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential cross-origin PII leak via FedCM Autofill integration

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.

Overview: When FedCM autofill integration is enabled, a cross-origin iframe can access Identity Provider suggestions (including email and name) from a pending FedCM request initiated by the top-level frame. This bypasses permissions policy checks and fills the PII into the iframe after presenting a confusing modal that attributes the request to the top-level origin.

Affected files:

  • components/autofill/content/browser/suggestions/identity_credential_suggestion_generator.cc
  • components/autofill/content/browser/content_identity_credential_delegate.cc
  • content/browser/webid/request_service.cc
  • components/autofill/core/browser/ui/autofill_external_delegate.cc
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc

Estimated timestamp from git blame: 2025-08-26

Summary

A vulnerability in the FedCM autofill integration allows a cross-origin iframe to obtain a user’s private identity information (email, full name) provided by an Identity Provider (IdP). This occurs because pending FedCM requests are stored per-Page but are accessible to any RenderFrameHost within that page via the Autofill subsystem, without verifying the requesting frame’s origin or permissions policy. The issue is currently gated behind the experimental kFedCmAutofill or kFedCmDelegation flags.

Technical Details

When a frame initiates a conditional FedCM request, RequestService::RequestToken verifies the identity-credentials-get permissions policy for the initiating frame. The RequestService is then registered as the pending web identity request for the shared Page object via webid::RequestPageData.

When a user interacts with a form field marked with autocomplete="webidentity" in any frame (including a cross-origin iframe), the BrowserAutofillManager requests verified suggestions from ContentIdentityCredentialDelegate. The delegate locates the pending FedCM request by calling AutofillSource::FromPage(web_contents->GetPrimaryPage()), retrieving the request initiated by the top-level page.

The IdentityCredentialSuggestionGenerator then fetches the account data from the AutofillSource (RequestService). Critically, RequestService::GetAutofillSuggestions() returns the IdP account data without checking if the frame requesting the suggestions matches the frame that initiated the request, completely bypassing the permissions policy check for the iframe.

Upon user selection of a suggestion, RequestService::NotifyAutofillSuggestionAccepted displays a confirmation modal. The UI displays the Relying Party as the top-level origin (using GetTopFrameOriginForDisplay), deceiving the user into believing they are sharing data with the trusted embedding site. After confirmation, AutofillExternalDelegate triggers a fill operation. Because the autofill was triggered from the iframe, the triggered_origin matches the target field’s origin, allowing the fill to pass FormForest cross-frame security checks and inject the PII into the iframe’s DOM.

Potential Attack Scenario

  1. An attacker embeds a malicious iframe (https://evil.example) on a legitimate Relying Party (https://rp.example).
  2. The RP initiates a conditional FedCM request. The browser caches the IdP details at the Page level.
  3. The user clicks on an <input autocomplete="email webidentity"> field inside the evil.example iframe.
  4. The browser displays autofill suggestions containing the user’s IdP email.
  5. The user selects the suggestion. A modal appears stating “Sign in to rp.example with idp.example”.
  6. The user, trusting rp.example, clicks “Continue”.
  7. The email address is filled into the <input> element inside the evil.example iframe.
  8. Malicious JavaScript in the iframe reads the field value and exfiltrates the PII.

Suggested Fix

The core issue is that ContentIdentityCredentialDelegate fetches the AutofillSource using FromPage(), leaking the request across frame boundaries.

  1. Introduce AutofillSource::FromRenderFrameHost(RenderFrameHost*): This method should retrieve the pending request for the page, but explicitly verify that the request was initiated by the provided RenderFrameHost. Since RequestService is a DocumentUserData, it is inherently tied to a specific frame.
  2. Update Suggestion Generation: Modify ContentIdentityCredentialDelegate and IdentityCredentialSuggestionGenerator to pass the RenderFrameHost that triggered the autofill request (resolvable via trigger_field.host_frame() and the WebContents) to the new FromRenderFrameHost method.

This ensures that only the specific frame that initiated the FedCM request (and thus passed the initial permissions policy check) can retrieve and display the resulting autofill suggestions.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.

View on issue tracker