Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Payments
DescriptionInformation leak in Payments
ComponentPayments
Bug ClassLogic Error
Tracker502488051
Fix commitab229fbda722 (chromium/src) +49/-33
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl.cc
modified
if
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc
modified
TEST_F
components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc
modified

Files Changed

  • chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl.cc
  • chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc
  • components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc
  • components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator_unittest.cc
From ab229fbda7229dbdf35293cd2db938961eae46e7 Mon Sep 17 00:00:00 2001
From: Christoph Schwering <schwering@google.com>
Date: Thu, 23 Jul 2026 03:58:14 -0700
Subject: [PATCH] [Autofill] Check trigger field origin for loyalty card association

Bug: 502488051
Change-Id: I46980233b26bfc4e4d0735ca2f70a031b5d6e48c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8126509
Reviewed-by: Norge Vizcay <vizcay@google.com>
Commit-Queue: Christoph Schwering <schwering@google.com>
Cr-Commit-Position: refs/heads/main@{#1666982}
---

diff --git a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl.cc b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl.cc
index 23ab4d9..e86a6273 100644
--- a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl.cc
+++ b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl.cc
@@ -234,13 +234,13 @@
 
   // Only show the TTF surface if any loyalty card have a matching merchant
   // domain.
-  const GURL& current_domain =
-      manager_->client().GetLastCommittedPrimaryMainFrameURL();
-  if (std::ranges::any_of(
-          loyalty_cards, [&current_domain](const LoyaltyCard& loyalty_card) {
-            return loyalty_card.GetAffiliationCategory(current_domain) ==
-                   LoyaltyCard::AffiliationCategory::kAffiliated;
-          })) {
+  if (std::ranges::any_of(loyalty_cards, [&](const LoyaltyCard& loyalty_card) {
+        return loyalty_card.GetAffiliationCategory(
+                   manager_->client().GetLastCommittedPrimaryMainFrameURL()) ==
+                   LoyaltyCard::AffiliationCategory::kAffiliated &&
+               manager_->client().GetLastCommittedPrimaryMainFrameOrigin() ==
+                   query_field_.origin();
+      })) {
     return DryRunResult(TriggerOutcome::kShown, loyalty_cards);
   }
   return DryRunResult(TriggerOutcome::kNoValidPaymentMethods, {});
diff --git a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc
index b37c74c8..0869edd 100644
--- a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc
+++ b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc
@@ -43,6 +43,7 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 namespace autofill {
 namespace {
@@ -274,6 +275,13 @@
         });
   }
 
+  void set_last_committed_primary_main_frame_url(const GURL& url) {
+    autofill_client().set_last_committed_primary_main_frame_url(url);
+    for (auto& field : test_api(form_).fields()) {
+      field.set_origin(url::Origin::Create(url));
+    }
+  }
+
   // Helper method to add the given `card` and create a card form.
   void ConfigureForCreditCards(const CreditCard& card) {
     form_ = test::CreateTestCreditCardFormData(/*is_https=*/true,
@@ -307,8 +315,7 @@
     form_ = test::CreateTestLoyaltyCardFormData();
     test_api(form_).field(0).set_is_focusable(true);
     // The current URL matches the loyalty card merchant domain.
-    autofill_client().set_last_committed_primary_main_frame_url(
-        GURL("https://domain.example"));
+    set_last_committed_primary_main_frame_url(GURL("https://domain.example"));
   }
 
   void OnFormsSeen() {
@@ -822,8 +829,7 @@
 TEST_F(TouchToFillPaymentMethodDelegateAndroidImplCreditCardUnitTest,
        TryToShowTouchToFillFailsIfClientIsNotSecure) {
   // Simulate non-secure client.
-  autofill_client().set_last_committed_primary_main_frame_url(
-      GURL("http://example.test"));
+  set_last_committed_primary_main_frame_url(GURL("http://example.test"));
 
   ASSERT_FALSE(touch_to_fill_delegate_->IsShowingTouchToFill());
 
@@ -1431,8 +1437,7 @@
   LoyaltyCard card2 = test::CreateLoyaltyCard2();
   std::vector<LoyaltyCard> loyalty_cards{card2, card1};
   // Makes sure there is at least one affiliated card available.
-  autofill_client().set_last_committed_primary_main_frame_url(
-      card1.merchant_domains()[0]);
+  set_last_committed_primary_main_frame_url(card1.merchant_domains()[0]);
   test_api(*autofill_client().GetValuablesDataManager())
       .SetLoyaltyCards(loyalty_cards);
 
@@ -1445,7 +1450,7 @@
 
 TEST_F(TouchToFillPaymentMethodDelegateAndroidImplLoyaltyCardUnitTest,
        TryToShowTouchToFillFailsIfNoMatchingDomains) {
-  autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://non-matching.domain"));
   std::vector<LoyaltyCard> loyalty_cards{test::CreateLoyaltyCard()};
   test_api(*autofill_client().GetValuablesDataManager())
@@ -1551,8 +1556,7 @@
     form_ = test::CreateTestEmailOrLoyaltyCardFormData();
     test_api(form_).field(0).set_is_focusable(true);
     // The current URL matches the loyalty card merchant domain.
-    autofill_client().set_last_committed_primary_main_frame_url(
-        GURL("https://domain.example"));
+    set_last_committed_primary_main_frame_url(GURL("https://domain.example"));
   }
 };
 
@@ -1564,8 +1568,7 @@
   LoyaltyCard card2 = test::CreateLoyaltyCard2();
   std::vector<LoyaltyCard> loyalty_cards{card2, card1};
   // Makes sure there is at least one affiliated card available.
-  autofill_client().set_last_committed_primary_main_frame_url(
-      card1.merchant_domains()[0]);
+  set_last_committed_primary_main_frame_url(card1.merchant_domains()[0]);
   test_api(*autofill_client().GetValuablesDataManager())
       .SetLoyaltyCards(loyalty_cards);
 
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 ed33fe12..1de9ba2 100644
--- a/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc
+++ b/components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc
@@ -326,7 +326,9 @@
       all_loyalty_cards, [&](const LoyaltyCard& card) {
         return card.GetAffiliationCategory(
                    client.GetLastCommittedPrimaryMainFrameURL()) ==
-               LoyaltyCard::AffiliationCategory::kAffiliated;
+                   LoyaltyCard::AffiliationCategory::kAffiliated &&
+               client.GetLastCommittedPrimaryMainFrameOrigin() ==
+                   trigger_field.origin();
       });
 
   base::span<const LoyaltyCard> affiliated_cards =
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 5416097..e00e81dd 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
@@ -27,6 +27,8 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/gfx/image/image_unittest_util.h"
+#include "url/gurl.h"
+#include "url/origin.h"
 
 namespace autofill {
 
@@ -122,6 +124,15 @@
     test_api(*form_structure_).SetFieldTypes({LOYALTY_MEMBERSHIP_ID});
   }
 
+  void set_last_committed_primary_main_frame_url(const GURL& url) {
+    test_autofill_client().set_last_committed_primary_main_frame_url(url);
+    if (form_structure_) {
+      for (const auto& field : form_structure_->fields()) {
+        field->set_origin(url::Origin::Create(url));
+      }
+    }
+  }
+
   TestAutofillClient& test_autofill_client() { return autofill_client_; }
   AutofillClient& client() { return autofill_client_; }
   FormStructure& form() { return *form_structure_; }
@@ -142,7 +153,7 @@
 };
 
 TEST_F(ValuableSuggestionGeneratorTest, NoMatchingDomain) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://not-existing-domain.example/test"));
   EXPECT_THAT(GetSuggestionsForLoyaltyCards(
                   form().ToFormData(), &form(), field(), &field(),
@@ -151,7 +162,7 @@
 }
 
 TEST_F(ValuableSuggestionGeneratorTest, NoMatchingDomainAndFieldAutofilled) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://not-existing-domain.example/test"));
   field().AddFieldModifier(FieldModifier::kAutofill);
   EXPECT_THAT(GetSuggestionsForLoyaltyCards(
@@ -161,7 +172,7 @@
 }
 
 TEST_F(ValuableSuggestionGeneratorTest, WithMatchingDomain) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain2.example/test"));
   std::vector<Suggestion> suggestions_with_matching_domain =
       GetSuggestionsForLoyaltyCards(form().ToFormData(), &form(), field(),
@@ -201,7 +212,7 @@
 }
 
 TEST_F(ValuableSuggestionGeneratorTest, WithMatchingDomainAndFieldAutofilled) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc
index b37c74c8..0869edd 100644
--- a/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc
+++ b/chrome/browser/touch_to_fill/autofill/android/touch_to_fill_payment_method_delegate_android_impl_unittest.cc
@@ -43,6 +43,7 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 namespace autofill {
 namespace {
@@ -274,6 +275,13 @@
         });
   }
 
+  void set_last_committed_primary_main_frame_url(const GURL& url) {
+    autofill_client().set_last_committed_primary_main_frame_url(url);
+    for (auto& field : test_api(form_).fields()) {
+      field.set_origin(url::Origin::Create(url));
+    }
+  }
+
   // Helper method to add the given `card` and create a card form.
   void ConfigureForCreditCards(const CreditCard& card) {
     form_ = test::CreateTestCreditCardFormData(/*is_https=*/true,
@@ -307,8 +315,7 @@
     form_ = test::CreateTestLoyaltyCardFormData();
     test_api(form_).field(0).set_is_focusable(true);
     // The current URL matches the loyalty card merchant domain.
-    autofill_client().set_last_committed_primary_main_frame_url(
-        GURL("https://domain.example"));
+    set_last_committed_primary_main_frame_url(GURL("https://domain.example"));
   }
 
   void OnFormsSeen() {
@@ -822,8 +829,7 @@
 TEST_F(TouchToFillPaymentMethodDelegateAndroidImplCreditCardUnitTest,
        TryToShowTouchToFillFailsIfClientIsNotSecure) {
   // Simulate non-secure client.
-  autofill_client().set_last_committed_primary_main_frame_url(
-      GURL("http://example.test"));
+  set_last_committed_primary_main_frame_url(GURL("http://example.test"));
 
   ASSERT_FALSE(touch_to_fill_delegate_->IsShowingTouchToFill());
 
@@ -1431,8 +1437,7 @@
   LoyaltyCard card2 = test::CreateLoyaltyCard2();
   std::vector<LoyaltyCard> loyalty_cards{card2, card1};
   // Makes sure there is at least one affiliated card available.
-  autofill_client().set_last_committed_primary_main_frame_url(
-      card1.merchant_domains()[0]);
+  set_last_committed_primary_main_frame_url(card1.merchant_domains()[0]);
   test_api(*autofill_client().GetValuablesDataManager())
       .SetLoyaltyCards(loyalty_cards);
 
@@ -1445,7 +1450,7 @@
 
 TEST_F(TouchToFillPaymentMethodDelegateAndroidImplLoyaltyCardUnitTest,
        TryToShowTouchToFillFailsIfNoMatchingDomains) {
-  autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://non-matching.domain"));
   std::vector<LoyaltyCard> loyalty_cards{test::CreateLoyaltyCard()};
   test_api(*autofill_client().GetValuablesDataManager())
@@ -1551,8 +1556,7 @@
     form_ = test::CreateTestEmailOrLoyaltyCardFormData();
     test_api(form_).field(0).set_is_focusable(true);
     // The current URL matches the loyalty card merchant domain.
-    autofill_client().set_last_committed_primary_main_frame_url(
-        GURL("https://domain.example"));
+    set_last_committed_primary_main_frame_url(GURL("https://domain.example"));
   }
 };
 
@@ -1564,8 +1568,7 @@
   LoyaltyCard card2 = test::CreateLoyaltyCard2();
   std::vector<LoyaltyCard> loyalty_cards{card2, card1};
   // Makes sure there is at least one affiliated card available.
-  autofill_client().set_last_committed_primary_main_frame_url(
-      card1.merchant_domains()[0]);
+  set_last_committed_primary_main_frame_url(card1.merchant_domains()[0]);
   test_api(*autofill_client().GetValuablesDataManager())
       .SetLoyaltyCards(loyalty_cards);
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 5416097..e00e81dd 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
@@ -27,6 +27,8 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/gfx/image/image_unittest_util.h"
+#include "url/gurl.h"
+#include "url/origin.h"
 
 namespace autofill {
 
@@ -122,6 +124,15 @@
     test_api(*form_structure_).SetFieldTypes({LOYALTY_MEMBERSHIP_ID});
   }
 
+  void set_last_committed_primary_main_frame_url(const GURL& url) {
+    test_autofill_client().set_last_committed_primary_main_frame_url(url);
+    if (form_structure_) {
+      for (const auto& field : form_structure_->fields()) {
+        field->set_origin(url::Origin::Create(url));
+      }
+    }
+  }
+
   TestAutofillClient& test_autofill_client() { return autofill_client_; }
   AutofillClient& client() { return autofill_client_; }
   FormStructure& form() { return *form_structure_; }
@@ -142,7 +153,7 @@
 };
 
 TEST_F(ValuableSuggestionGeneratorTest, NoMatchingDomain) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://not-existing-domain.example/test"));
   EXPECT_THAT(GetSuggestionsForLoyaltyCards(
                   form().ToFormData(), &form(), field(), &field(),
@@ -151,7 +162,7 @@
 }
 
 TEST_F(ValuableSuggestionGeneratorTest, NoMatchingDomainAndFieldAutofilled) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://not-existing-domain.example/test"));
   field().AddFieldModifier(FieldModifier::kAutofill);
   EXPECT_THAT(GetSuggestionsForLoyaltyCards(
@@ -161,7 +172,7 @@
 }
 
 TEST_F(ValuableSuggestionGeneratorTest, WithMatchingDomain) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain2.example/test"));
   std::vector<Suggestion> suggestions_with_matching_domain =
       GetSuggestionsForLoyaltyCards(form().ToFormData(), &form(), field(),
@@ -201,7 +212,7 @@
 }
 
 TEST_F(ValuableSuggestionGeneratorTest, WithMatchingDomainAndFieldAutofilled) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain2.example/test"));
   field_data().set_is_autofilled_according_to_renderer(true);
   std::vector<Suggestion> suggestions_with_matching_domain =
@@ -243,7 +254,7 @@
 }
 
 TEST_F(ValuableSuggestionGeneratorTest, AllMatchDomain) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://common-domain.example/test"));
   EXPECT_THAT(
       GetSuggestionsForLoyaltyCards(form().ToFormData(), &form(), field(),
@@ -275,7 +286,7 @@
           /*use_date=*/{}, /*use_count=*/0));
   valuables_data_manager().CacheImage(program_logo, fake_image);
   test_api(valuables_data_manager()).NotifyObservers();
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain1.example/test"));
 
   std::vector<Suggestion> suggestions = GetSuggestionsForLoyaltyCards(
@@ -539,7 +550,7 @@
 
   raw_ptr<const base::Feature> kIphFeature =
       &feature_engagement::kIPHAutofillEnableLoyaltyCardsFeature;
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain1.example/test"));
   EXPECT_THAT(GetSuggestionsForLoyaltyCards(
                   form().ToFormData(), &form(), field(), &field(),
@@ -567,7 +578,7 @@
 
   raw_ptr<const base::Feature> kIphFeature =
       &feature_engagement::kIPHAutofillAiValuablesFeature;
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain1.example/test"));
   EXPECT_THAT(GetSuggestionsForLoyaltyCards(
                   form().ToFormData(), &form(), field(), &field(),
@@ -579,7 +590,7 @@
 // Checks that all loyalty cards are returned as suggestion data, and
 // used for generating suggestions.
 TEST_F(ValuableSuggestionGeneratorTest, GeneratesLoyaltyCardSuggestions) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://common-domain.example/test"));
 
   base::MockCallback<
@@ -616,7 +627,7 @@
 TEST_F(
     ValuableSuggestionGeneratorWithNonAffiliationSupportTest,
     ValuableSuggestionGeneratorWithNonAffiliationSupportTest_NoMatchingDomain_NoPasswordForm) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://not-existing-domain.example/test"));
   std::vector<Suggestion> suggestions = GetSuggestionsForLoyaltyCards(
       form().ToFormData(), &form(), field(), &field(),
@@ -648,7 +659,7 @@
 TEST_F(
     ValuableSuggestionGeneratorWithNonAffiliationSupportTest,
     ValuableSuggestionGeneratorWithNonAffiliationSupportTest_WithMatchingDomain) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain2.example/test"));
   std::vector<Suggestion> suggestions_with_matching_domain =
       GetSuggestionsForLoyaltyCards(form().ToFormData(), &form(), field(),
@@ -698,7 +709,7 @@
 TEST_F(
     ValuableSuggestionGeneratorWithNonAffiliationSupportTest,
     ValuableSuggestionGeneratorWithNonAffiliationSupportTest_AllMatchDomain) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://common-domain.example/test"));
   EXPECT_THAT(
       GetSuggestionsForLoyaltyCards(form().ToFormData(), &form(), field(),
@@ -717,7 +728,7 @@
 
 TEST_F(ValuableSuggestionGeneratorWithNonAffiliationSupportTest,
        WithMatchingDomain_OnPasswordForm) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://domain2.example/test"));
   std::vector<Suggestion> suggestions_with_matching_domain =
       GetSuggestionsForLoyaltyCards(
@@ -768,7 +779,7 @@
 
 TEST_F(ValuableSuggestionGeneratorWithNonAffiliationSupportTest,
        NoMatchingDomain_OnPasswordForm) {
-  test_autofill_client().set_last_committed_primary_main_frame_url(
+  set_last_committed_primary_main_frame_url(
       GURL("https://not-existing-domain.example/test"));
   EXPECT_THAT(GetSuggestionsForLoyaltyCards(
                   form().ToFormData(), &form(), field(), &field(),
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential cross-origin loyalty card data leak via Autofill affiliation check

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: Chrome’s Autofill functionality for loyalty cards potentially leaks sensitive data to cross-origin subframes. The suggestion generation logic checks affiliation against the main frame’s URL rather than the requesting subframe’s origin. Furthermore, the single-field filling path bypasses cross-origin security checks, allowing an embedded iframe to steal loyalty card numbers.

Affected files:

  • chrome/browser/touch_to_fill/autofill/android/touch_to_fill_delegate_android_impl.cc
  • components/autofill/core/browser/suggestions/valuables/valuable_suggestion_generator.cc
  • chrome/browser/ui/autofill/payments/chrome_payments_autofill_client.cc
  • components/autofill/core/browser/foundations/autofill_driver_router.cc
  • components/autofill/content/browser/content_autofill_driver.cc

Estimated timestamp from git blame: 2025-10-29

Description

There is a potential vulnerability in Chrome’s Autofill implementation for Google Wallet loyalty cards that allows data to be leaked to cross-origin iframes. The issue stems from two interconnected logic flaws: an incorrect origin check during suggestion generation, and a lack of cross-origin security validation during single-field filling.

1. Incorrect Affiliation Check

When a user interacts with a form field, the browser evaluates available Autofill suggestions. For loyalty cards, this logic is handled in LoyaltyCardSuggestionGenerator::GenerateSuggestions() (Desktop) and TouchToFillDelegateAndroidImpl::DryRunForAffiliatedLoyaltyCard() (Android).

In both instances, Chrome determines if a loyalty card is affiliated with the current context by calling client.GetLastCommittedPrimaryMainFrameURL(). Because this uses the top-level frame’s URL, it ignores the actual origin of the frame requesting the data (trigger_field.origin()). If a user is on https://merchant.com, any embedded cross-origin iframe (e.g., https://attacker.example) will be treated as merchant.com for the purpose of loyalty card affiliation.

2. Bypassed Fill-Time Security Checks

When a user selects the loyalty card from the trusted Chrome UI, the acceptance logic (e.g., AutofillExternalDelegate::DidAcceptSuggestion or TouchToFillDelegateAndroidImpl::LoyaltyCardSuggestionSelected) calls manager_->FillOrPreviewField().

This execution flows through FormFiller::FillOrPreviewField() to AutofillDriverRouter::ApplyFieldAction(). Crucially, ApplyFieldAction is designed for single-field operations and acts purely as a routing mechanism. Unlike multi-field form filling (ApplyFormAction), which rigorously evaluates FormForest::SecurityOptions and the IsSafeToFill cross-origin policy, ApplyFieldAction bypasses these checks entirely. It blindly sends the cleartext loyalty card number via IPC to the renderer belonging to the triggering frame.

Potential Attack Scenario

Note: These are suggested steps based on static code analysis; our tooling agent does not have the ability to run active exploit code to verify them dynamically.

  1. A victim visits a legitimate merchant site (https://merchant.com) for which they have a saved Google Wallet loyalty card.
  2. The merchant site embeds a malicious or compromised cross-origin iframe (https://attacker.example).
  3. The attacker’s iframe contains an input field crafted to match loyalty card Autofill heuristics (e.g., <input name="loyalty_number">).
  4. The user clicks or taps on this input field within the iframe.
  5. Chrome receives the Autofill request from the attacker.example subframe but incorrectly evaluates affiliation using the merchant.com main frame URL.
  6. Chrome displays a trusted UI (like the Android Touch-to-Fill bottom sheet) offering the user’s merchant.com loyalty card.
  7. The user, trusting the UI and context, selects the card.
  8. Chrome routes the single-field fill action directly to the attacker.example driver without secondary security checks, filling the input field with the cleartext loyalty card number.
  9. Malicious JavaScript in the iframe reads the field value and exfiltrates the data.

Suggested Fix

  1. Correct Suggestion Scoping: Update LoyaltyCardSuggestionGenerator::GenerateSuggestions and TouchToFillDelegateAndroidImpl::DryRunForAffiliatedLoyaltyCard to evaluate affiliation against the trigger_field.origin() instead of GetLastCommittedPrimaryMainFrameURL(). If a loyalty card is not explicitly affiliated with the subframe’s origin, it should not be suggested.
  2. Enforce Single-Field Security: Implement cross-origin security checks in the single-field filling path (either in FormFiller::FillOrPreviewField or AutofillDriverRouter::ApplyFieldAction) to ensure that sensitive data is never dispatched to an origin that does not match the affiliation of the credential being filled.

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
Links in the report