Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Autofill
DescriptionIncorrect authorization in Autofill
ComponentAutofill
Sub-componentUI>Browser>Autofill
Bug ClassAuthorization Error
Tracker540072162
Fix commit0922e369cc37 (chromium/src) +44/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
GURL
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h
modified
Origin
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h
modified
ValuablesDataManager
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h
modified
LoyaltyCardSuggestionGenerator
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h
modified

Files Changed

  • components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.h
  • components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc
  • components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc
From 0922e369cc37190c7b9cf25e0c5c2fb1fe9d0a44 Mon Sep 17 00:00:00 2001
From: Norge Vizcay <vizcay@google.com>
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 <schwering@google.com>
Commit-Queue: Norge Vizcay <vizcay@google.com>
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.
Loading diff…

Regression Test / PoC

shipped with the fix
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.
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • components/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.

  1. A victim has at least one Google Wallet loyalty card for merchant.com and at least one saved Autofill address profile.
  2. The user navigates to https://merchant.com, which commits as the primary main frame.
  3. The merchant page embeds a cross-origin iframe hosted on https://attacker.example/.
  4. The iframe contains an input field whose attributes trigger classification as an email or loyalty card field (EMAIL_OR_LOYALTY_MEMBERSHIP_ID).
  5. The victim focuses on the input inside the iframe.
  6. Chrome generates profile suggestions for the field (making suggestions non-empty), and subsequently triggers the legacy merge flow.
  7. Due to the missing origin check, CreateLoyaltyCardSuggestionsForMerge filters cards against the main frame URL (https://merchant.com/), and merges the merchant.com loyalty card suggestion into the suggestion list shown on the iframe.
  8. The victim, seeing the merchant’s branding, selects the loyalty card suggestion.
  9. AutofillExternalDelegate::DidAcceptSuggestion calls FillOrPreviewField, which routes the value fill directly to the iframe’s driver with no origin-boundary checks.
  10. 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.

View on issue tracker
Links in the report