CVE-2026-106212
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
GURLcomponents/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h |
modified | |
Origincomponents/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h |
modified | |
ValuablesDataManagercomponents/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h |
modified | |
LoyaltyCardSuggestionGeneratorcomponents/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h |
modified |
Files Changed
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.hcomponents/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cccomponents/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.cc
Patch
From 0922e369cc37190c7b9cf25e0c5c2fb1fe9d0a44 Mon Sep 17 00:00:00 2001 From: Norge Vizcay <[email protected]> Date: Mon, 14 Sep 2026 10:11:22 -0700 Subject: [PATCH] Enforce origin verification for merged loyalty card suggestions Verify that the trigger field's origin matches the primary main frame's origin before merging Google Wallet loyalty card suggestions. Previously, CreateLoyaltyCardSuggestionsForMerge() checked only the primary main frame's URL without validating the trigger field origin, allowing cross-origin iframes embedded in affiliated merchant pages to receive top-level domain loyalty card suggestions on EMAIL_OR_LOYALTY_MEMBERSHIP_ID fields upon user selection. Specifically: - Update CreateLoyaltyCardSuggestionsForMerge() to accept trigger_field_origin and return empty suggestions if the origin of main_frame_url does not match trigger_field_origin. - Update existing unit tests in valuable_suggestion_generator_unittest.cc and add ExtendEmailSuggestionsWithLoyaltyCardSuggestions_OriginMismatch to verify that cross-origin merge suggestions are suppressed. TAG=agy CONV=205205f8-676b-4e0b-b8e5-ced82306db94 Fixed: 540072162 Change-Id: Ib2fd936bf95a9cf057d94e946d8025f152933a20 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8398011 Reviewed-by: Christoph Schwering <[email protected]> Commit-Queue: Norge Vizcay <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697313} --- diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager.cc b/components/autofill/core/browser/foundations/browser_autofill_manager.cc index 2816a92..1fad37d2 100644 --- a/components/autofill/core/browser/foundations/browser_autofill_manager.cc +++ b/components/autofill/core/browser/foundations/browser_autofill_manager.cc @@ -3193,7 +3193,8 @@ std::vector<Suggestion> loyalty_cards_suggestions_for_merge = CreateLoyaltyCardSuggestionsForMerge( *valuables_manager, - client().GetLastCommittedPrimaryMainFrameURL()); + client().GetLastCommittedPrimaryMainFrameURL(), + field.origin()); MergeLoyaltyCardsAndAddressSuggestions( suggestions, std::move(loyalty_cards_suggestions_for_merge)); } diff --git a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc index 8c10c83d..09e4d76 100644 --- a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc +++ b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc @@ -42,6 +42,7 @@ #include "ui/base/l10n/l10n_util.h" #include "ui/gfx/image/image.h" #include "url/gurl.h" +#include "url/origin.h" namespace autofill { namespace { @@ -177,7 +178,11 @@ std::vector<Suggestion> CreateLoyaltyCardSuggestionsForMerge( const ValuablesDataManager& valuables_manager, - const GURL& url) { + const GURL& main_frame_url, + const url::Origin& trigger_field_origin) { + if (url::Origin::Create(main_frame_url) != trigger_field_origin) { + return {}; + } std::vector<LoyaltyCard> all_loyalty_cards = valuables_manager.GetLoyaltyCardsToSuggest(); if (all_loyalty_cards.empty()) { @@ -188,7 +193,7 @@ std::copy_if(all_loyalty_cards.begin(), all_loyalty_cards.end(), std::back_inserter(affiliated_cards), [&](const LoyaltyCard& card) { - return card.GetAffiliationCategory(url) == + return card.GetAffiliationCategory(main_frame_url) == LoyaltyCard::AffiliationCategory::kAffiliated; }); if (affiliated_cards.empty()) { diff --git a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h index 492e72f..96adadbb 100644 --- a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h +++ b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h @@ -19,6 +19,10 @@ class GURL; +namespace url { +class Origin; +} + namespace autofill { class ValuablesDataManager; @@ -51,7 +55,8 @@ // launched. std::vector<Suggestion> CreateLoyaltyCardSuggestionsForMerge( const ValuablesDataManager& valuables_manager, - const GURL& url); + const GURL& main_frame_url, + const url::Origin& trigger_field_origin); class LoyaltyCardSuggestionGenerator : public SuggestionGenerator { public: diff --git a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc index 8fbb57edf..e4c94c91 100644 --- a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc +++ b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc @@ -343,7 +343,9 @@ std::vector<Suggestion> loyalty_card_suggestions = CreateLoyaltyCardSuggestionsForMerge( valuables_data_manager(), - GURL("https://common-matching-domain.example/test")); + GURL("https://common-matching-domain.example/test"), + url::Origin::Create( + GURL("https://common-matching-domain.example/test"))); MergeLoyaltyCardsAndAddressSuggestions(email_suggestions, std::move(loyalty_card_suggestions)); @@ -412,7 +414,8 @@ std::vector<Suggestion> loyalty_card_suggestions = CreateLoyaltyCardSuggestionsForMerge( - valuables_data_manager(), GURL("https://common-domain.example/test")); + valuables_data_manager(), GURL("https://common-domain.example/test"), + url::Origin::Create(GURL("https://common-domain.example/test"))); MergeLoyaltyCardsAndAddressSuggestions(email_suggestions, std::move(loyalty_card_suggestions)); @@ -470,7 +473,9 @@ std::vector<Suggestion> loyalty_card_suggestions = CreateLoyaltyCardSuggestionsForMerge( valuables_data_manager(), - GURL("https://common-matching-domain.example/test")); + GURL("https://common-matching-domain.example/test"), + url::Origin::Create( + GURL("https://common-matching-domain.example/test"))); MergeLoyaltyCardsAndAddressSuggestions(email_suggestions, std::move(loyalty_card_suggestions)); @@ -529,6 +534,27 @@ #endif // BUILDFLAG(IS_ANDROID) } +TEST_F(ValuableSuggestionGeneratorTest, + ExtendEmailSuggestionsWithLoyaltyCardSuggestions_OriginMismatch) { + const std::vector<LoyaltyCard> loyalty_cards = {LoyaltyCard( + /*loyalty_card_id=*/ValuableId("loyalty_card_id_1"), + /*merchant_name=*/"CVS Pharmacy", + /*program_name=*/"CVS Extra", + /*program_logo=*/GURL("https://empty.url.com"), + /*loyalty_card_number=*/"987654321987654321", + {GURL("https://domain1.example"), + GURL("https://common-matching-domain.example")}, + /*use_date=*/{}, /*use_count=*/0)}; + test_api(valuables_data_manager()).SetLoyaltyCards(loyalty_cards); + + std::vector<Suggestion> loyalty_card_suggestions = + CreateLoyaltyCardSuggestionsForMerge( + valuables_data_manager(), + GURL("https://common-matching-domain.example/test"), + url::Origin::Create(GURL("https://cross-origin.example"))); + EXPECT_TRUE(loyalty_card_suggestions.empty()); +} + // TODO(crbug.com/431155933): Remove this test when cleaning up the feature. // The test is subsumed by GetSuggestionsForLoyaltyCards_SuggestionsUpdatedIPH // right below, whose name should be updated when cleaning up the feature.
Regression Test / PoC
diff --git a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc
index 8fbb57edf..e4c94c91 100644
--- a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc
+++ b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc
@@ -343,7 +343,9 @@
std::vector<Suggestion> loyalty_card_suggestions =
CreateLoyaltyCardSuggestionsForMerge(
valuables_data_manager(),
- GURL("https://common-matching-domain.example/test"));
+ GURL("https://common-matching-domain.example/test"),
+ url::Origin::Create(
+ GURL("https://common-matching-domain.example/test")));
MergeLoyaltyCardsAndAddressSuggestions(email_suggestions,
std::move(loyalty_card_suggestions));
@@ -412,7 +414,8 @@
std::vector<Suggestion> loyalty_card_suggestions =
CreateLoyaltyCardSuggestionsForMerge(
- valuables_data_manager(), GURL("https://common-domain.example/test"));
+ valuables_data_manager(), GURL("https://common-domain.example/test"),
+ url::Origin::Create(GURL("https://common-domain.example/test")));
MergeLoyaltyCardsAndAddressSuggestions(email_suggestions,
std::move(loyalty_card_suggestions));
@@ -470,7 +473,9 @@
std::vector<Suggestion> loyalty_card_suggestions =
CreateLoyaltyCardSuggestionsForMerge(
valuables_data_manager(),
- GURL("https://common-matching-domain.example/test"));
+ GURL("https://common-matching-domain.example/test"),
+ url::Origin::Create(
+ GURL("https://common-matching-domain.example/test")));
MergeLoyaltyCardsAndAddressSuggestions(email_suggestions,
std::move(loyalty_card_suggestions));
@@ -529,6 +534,27 @@
#endif // BUILDFLAG(IS_ANDROID)
}
+TEST_F(ValuableSuggestionGeneratorTest,
+ ExtendEmailSuggestionsWithLoyaltyCardSuggestions_OriginMismatch) {
+ const std::vector<LoyaltyCard> loyalty_cards = {LoyaltyCard(
+ /*loyalty_card_id=*/ValuableId("loyalty_card_id_1"),
+ /*merchant_name=*/"CVS Pharmacy",
+ /*program_name=*/"CVS Extra",
+ /*program_logo=*/GURL("https://empty.url.com"),
+ /*loyalty_card_number=*/"987654321987654321",
+ {GURL("https://domain1.example"),
+ GURL("https://common-matching-domain.example")},
+ /*use_date=*/{}, /*use_count=*/0)};
+ test_api(valuables_data_manager()).SetLoyaltyCards(loyalty_cards);
+
+ std::vector<Suggestion> loyalty_card_suggestions =
+ CreateLoyaltyCardSuggestionsForMerge(
+ valuables_data_manager(),
+ GURL("https://common-matching-domain.example/test"),
+ url::Origin::Create(GURL("https://cross-origin.example")));
+ EXPECT_TRUE(loyalty_card_suggestions.empty());
+}
+
// TODO(crbug.com/431155933): Remove this test when cleaning up the feature.
// The test is subsumed by GetSuggestionsForLoyaltyCards_SuggestionsUpdatedIPH
// right below, whose name should be updated when cleaning up the feature.
Original Bug Report
Wallet loyalty card suggestion merge bypasses origin verification for subframes
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: In the legacy Autofill path, Google Wallet loyalty card suggestions are filtered and merged based solely on the main-frame URL instead of the trigger field’s frame origin. Consequently, a potential cross-origin iframe embedded on an affiliated merchant page can trigger and receive the victim’s Wallet loyalty card suggestions. If the user accepts the suggestion, the loyalty card number is filled directly into the cross-origin iframe’s field with no origin-boundary checks.
Affected files:
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.cc
Estimated timestamp from git blame: 2025-05-16
Description
There is a potential origin verification bypass in Chrome’s legacy Autofill suggestions merging flow. When a user focuses a form field in a cross-origin iframe, Google Wallet loyalty card suggestions associated with the primary top-level main-frame domain can be merged with address suggestions and filled into the iframe’s text field.
Root Cause
In components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc, CreateLoyaltyCardSuggestionsForMerge filters stored loyalty cards by checking whether they are affiliated with the passed URL, but performs no verification of the trigger field’s origin:
// components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc
std::vector<Suggestion> CreateLoyaltyCardSuggestionsForMerge(
const ValuablesDataManager& valuables_manager,
const GURL& url) {
std::vector<LoyaltyCard> all_loyalty_cards =
valuables_manager.GetLoyaltyCardsToSuggest();
...
std::vector<LoyaltyCard> affiliated_cards;
std::copy_if(all_loyalty_cards.begin(), all_loyalty_cards.end(),
std::back_inserter(affiliated_cards),
[&](const LoyaltyCard& card) {
return card.GetAffiliationCategory(url) ==
LoyaltyCard::AffiliationCategory::kAffiliated;
});
The sole caller in components/autofill/core/browser/foundations/browser_autofill_manager.cc passes the primary main-frame URL:
// components/autofill/core/browser/foundations/browser_autofill_manager.cc
std::vector<Suggestion> loyalty_cards_suggestions_for_merge =
CreateLoyaltyCardSuggestionsForMerge(
*valuables_manager,
client().GetLastCommittedPrimaryMainFrameURL());
MergeLoyaltyCardsAndAddressSuggestions(
suggestions, std::move(loyalty_cards_suggestions_for_merge));
Because the trigger field’s origin is never checked against the primary main frame’s origin (unlike in the patched sibling functions LoyaltyCardSuggestionGenerator::GenerateSuggestions and Touch-to-Fill payment methods), a cross-origin iframe can receive these suggestions.
Potential Trigger Steps
Please note that these are potential, suggested steps derived from static analysis, as our dynamic analysis environment cannot run compiled code.
- A victim has at least one Google Wallet loyalty card for
merchant.comand at least one saved Autofill address profile. - The user navigates to
https://merchant.com, which commits as the primary main frame. - The merchant page embeds a cross-origin iframe hosted on
https://attacker.example/. - The iframe contains an input field whose attributes trigger classification as an email or loyalty card field (
EMAIL_OR_LOYALTY_MEMBERSHIP_ID). - The victim focuses on the input inside the iframe.
- Chrome generates profile suggestions for the field (making
suggestionsnon-empty), and subsequently triggers the legacy merge flow. - Due to the missing origin check,
CreateLoyaltyCardSuggestionsForMergefilters cards against the main frame URL (https://merchant.com/), and merges themerchant.comloyalty card suggestion into the suggestion list shown on the iframe. - The victim, seeing the merchant’s branding, selects the loyalty card suggestion.
AutofillExternalDelegate::DidAcceptSuggestioncallsFillOrPreviewField, which routes the value fill directly to the iframe’s driver with no origin-boundary checks.- The loyalty card number is populated into the field, allowing the attacker-controlled iframe to read and exfiltrate it.
Suggested Fix
Before calling CreateLoyaltyCardSuggestionsForMerge or within the merging flow, enforce that the primary main frame’s origin matches the trigger field’s origin:
if (client().GetLastCommittedPrimaryMainFrameOrigin() == field.origin()) {
std::vector<Suggestion> loyalty_cards_suggestions_for_merge =
CreateLoyaltyCardSuggestionsForMerge(
*valuables_manager,
client().GetLastCommittedPrimaryMainFrameURL());
MergeLoyaltyCardsAndAddressSuggestions(
suggestions, std::move(loyalty_cards_suggestions_for_merge));
}
Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908
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.