Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Autofill
DescriptionInsufficient policy enforcement in Autofill
ComponentAutofill
Bug ClassLogic Error
Tracker501644835
Fix commit426b9288ffbe (chromium/src) +68/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
components/autofill/core/browser/filling/form_filler.cc
modified
RefillOptions
components/autofill/core/browser/filling/form_filler.h
modified
TEST_F
components/autofill/core/browser/filling/form_filler_unittest.cc
modified

Files Changed

  • components/autofill/core/browser/filling/form_filler.cc
  • components/autofill/core/browser/filling/form_filler.h
  • components/autofill/core/browser/filling/form_filler_unittest.cc
From 426b9288ffbe63736b28554b9bb6c122f4237e8f Mon Sep 17 00:00:00 2001
From: Ireneusz Szulc <[email protected]>
Date: Thu, 23 Apr 2026 16:40:38 -0700
Subject: [PATCH] Autofill: Fix reauth bypass in refill mechanism

This change ensures that Autofill Refills do not bypass the Payments
Mandatory Reauth feature for local credit cards.

Previously, refills allowed filling any field in a group if the group
was filled initially. This allowed a website to exploit refills by
initially asking for non-sensitive fields and later injecting a
sensitive field (eg. card number), which would be filled without user
interaction or re-authentication.

Now, we track the specific `FieldType`s that were originally filled in
the `RefillContext`. During a refill, we apply a specific rule for
credit cards: if the initial fill did not include the Credit Card Number
or CVC, the refill is not allowed to fill any field from this group.
This applies to all credit cards (local and server) to prevent both
security bypasses and bad use experience (filling masked data).
Note that filling Credit Card Number in the initial form, opens up
possibility to refill a CVC.

This implements the "sensitivity classification" approaach discussed in
the design doc.

Bug: 501644835
Change-Id: I837a3ece25c01327adc28b665e1e6d976a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7762042
Reviewed-by: Jihad Hanna <[email protected]>
Reviewed-by: Olivia Saul <[email protected]>
Commit-Queue: Ireneusz Szulc <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1619837}
---

diff --git a/components/autofill/core/browser/filling/form_filler.cc b/components/autofill/core/browser/filling/form_filler.cc
index 228ebff..85c12d8 100644
--- a/components/autofill/core/browser/filling/form_filler.cc
+++ b/components/autofill/core/browser/filling/form_filler.cc
@@ -524,8 +524,8 @@
   bool allows_automatic_refill = true;
   // The timer used to trigger a refill.
   base::OneShotTimer on_refill_timer;
-  // The field type groups that were initially filled.
-  DenseSet<FieldTypeGroup> type_groups_originally_filled;
+  // The field types that were initially filled.
+  FieldTypeSet types_originally_filled;
   // If populated, this map determines which values will be filled into a
   // field (it does not matter whether the field already contains a value).
   std::map<FieldGlobalId, FillingValueAndType> forced_fill_values;
@@ -549,7 +549,7 @@
 }
 
 FormFiller::RefillOptions FormFiller::RefillOptions::Refill(
-    DenseSet<FieldTypeGroup> originally_filled) {
+    FieldTypeSet originally_filled) {
   RefillOptions r;
   r.originally_filled_ = originally_filled;
   return r;
@@ -562,8 +562,26 @@
 bool FormFiller::RefillOptions::may_refill(
     const FieldTypeSet& field_types) const {
   CHECK(is_refill());
-  return originally_filled_->contains_all(
-      DenseSet<FieldTypeGroup>(field_types, &GroupTypeOfFieldType));
+  FieldTypeGroupSet requested_groups(field_types, &GroupTypeOfFieldType);
+  FieldTypeGroupSet filled_groups(*originally_filled_, &GroupTypeOfFieldType);
+  if (!filled_groups.contains_all(requested_groups)) {
+    return false;
+  }
+
+  // Rule for CCs: Filling other CC information without Credit Card Number or
+  // CVC does not allow refilling CCN/CVC.
+  if (requested_groups.contains(FieldTypeGroup::kCreditCard) ||
+      requested_groups.contains(FieldTypeGroup::kStandaloneCvcField)) {
+    auto contains_sensitive_cc = [](const FieldTypeSet& types) {
+      return types.contains_any({CREDIT_CARD_NUMBER,
+                                 CREDIT_CARD_VERIFICATION_CODE,
+                                 CREDIT_CARD_STANDALONE_VERIFICATION_CODE});
+    };
+    return contains_sensitive_cc(*originally_filled_) ||
+           !contains_sensitive_cc(field_types);
+  }
+
+  return true;
 }
 
 DenseSet<FieldFillingSkipReason> FormFiller::GetFillingSkipReasonsForField(
@@ -947,7 +965,7 @@
       !refill_trigger_reason;
   RefillOptions refill_options =
       refill_trigger_reason.has_value() && refill_context
-          ? RefillOptions::Refill(refill_context->type_groups_originally_filled)
+          ? RefillOptions::Refill(refill_context->types_originally_filled)
           : RefillOptions::NotRefill();
   if (refill_trigger_reason.has_value() && refill_context) {
     fill_id = refill_context->fill_id;
@@ -1031,8 +1049,7 @@
       filled_field_types.emplace(result_fields[i].global_id(),
                                  *filled_field_type);
       if (may_refill_in_future) {
-        refill_context->type_groups_originally_filled.insert_all(
-            autofill_field.Type().GetGroups());
+        refill_context->types_originally_filled.insert(*filled_field_type);
       }
     }
 
@@ -1272,7 +1289,8 @@
     case RefillTriggerReason::kSelectOptionsChanged:
       if (!field || !field->IsSelectElement() ||
           field->Type().GetGroups().contains_none(
-              refill_context->type_groups_originally_filled)) {
+              FieldTypeGroupSet(refill_context->types_originally_filled,
+                                &GroupTypeOfFieldType))) {
         // The element in question is not fillable as a result of this signal.
         // Do not trigger a refill as it would most likely be a trivial one.
         return;
diff --git a/components/autofill/core/browser/filling/form_filler.h b/components/autofill/core/browser/filling/form_filler.h
index cb813ea..264d1cb37 100644
--- a/components/autofill/core/browser/filling/form_filler.h
+++ b/components/autofill/core/browser/filling/form_filler.h
@@ -82,7 +82,7 @@
   class RefillOptions {
    public:
     static RefillOptions NotRefill();
-    static RefillOptions Refill(DenseSet<FieldTypeGroup> originally_filled);
+    static RefillOptions Refill(FieldTypeSet originally_filled);
 
     bool is_refill() const;
     bool may_refill(const FieldTypeSet& field_type) const;
@@ -90,7 +90,7 @@
    private:
     RefillOptions();
 
-    std::optional<DenseSet<FieldTypeGroup>> originally_filled_;
+    std::optional<FieldTypeSet> originally_filled_;
   };
 
   // Given `field`, the corresponding `autofill_field` to fill, and the
diff --git a/components/autofill/core/browser/filling/form_filler_unittest.cc b/components/autofill/core/browser/filling/form_filler_unittest.cc
index 971f7b9..deedb6e 100644
--- a/components/autofill/core/browser/filling/form_filler_unittest.cc
+++ b/components/autofill/core/browser/filling/form_filler_unittest.cc
@@ -25,6 +25,8 @@
 #include "components/autofill/core/browser/autofill_format_string.h"
 #include "components/autofill/core/browser/autofill_trigger_source.h"
 #include "components/autofill/core/browser/country_type.h"
+#include "components/autofill/core/browser/data_manager/payments/test_payments_data_manager.h"
+#include "components/autofill/core/browser/data_manager/test_personal_data_manager.h"
 #include "components/autofill/core/browser/data_model/addresses/autofill_profile.h"
 #include "components/autofill/core/browser/data_model/payments/credit_card.h"
 #include "components/autofill/core/browser/field_types.h"
@@ -2088,6 +2090,43 @@
   EXPECT_EQ(initial_fill_id, refill_fill_id);
 }
 
+// Tests that refills skip sensitive fields if the initial fill did not include
+// them.
+TEST_F(FormFillerTest, RefillSkipsSensitiveFieldsIfNotFilledInitially) {
+  CreditCard credit_card = test::GetCreditCard();
+  // Create form with a credit card name by making one with a name and number,
+  // then removing the number field. It will be readded after initial fill.
+  FormData form =
+      test::GetFormData({.fields = {{.role = CREDIT_CARD_NAME_FULL,
+                                     .autocomplete_attribute = "cc-name"},
+                                    {.role = CREDIT_CARD_NUMBER,
+                                     .autocomplete_attribute = "cc-number"}}});
+  FormFieldData number_field = form.fields().back();
+  test_api(form).fields().pop_back();
+  FormsSeen({form});
+
+  // Initial fill.
+  form = AutofillForm(form, form.fields().front(), &credit_card);
+
+  // Now add the number field back to trigger refill.
+  test_api(form).fields().push_back(std::move(number_field));
+
+  // Expect refill, but expect NO new fields to be filled because number field
+  // is sensitive and was not filled initially.
+  EXPECT_CALL(autofill_driver(), ApplyFormAction)
+      .WillOnce([&](mojom::FormActionType action_type,
+                    mojom::ActionPersistence action_persistence,
+                    base::span<const FormFieldData> data, const FillId& fill_id,
+                    bool supports_refill, const url::Origin& triggered_origin,
+                    const absl::flat_hash_map<FieldGlobalId, FieldType>&,
+                    const Section&) {
+        EXPECT_TRUE(data.empty());
+        return std::vector<FieldGlobalId>{};
+      });
+
+  FormsSeen({form});
+}
+
 // Tests that a programmatic refill can be triggered within the timeout.
 // Also tests that a second refill within the timeout is a no-op.
 TEST_F(FormFillerTest, ProgrammaticRefillBeforeTimeout) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/autofill/core/browser/filling/form_filler_unittest.cc b/components/autofill/core/browser/filling/form_filler_unittest.cc
index 971f7b9..deedb6e 100644
--- a/components/autofill/core/browser/filling/form_filler_unittest.cc
+++ b/components/autofill/core/browser/filling/form_filler_unittest.cc
@@ -25,6 +25,8 @@
 #include "components/autofill/core/browser/autofill_format_string.h"
 #include "components/autofill/core/browser/autofill_trigger_source.h"
 #include "components/autofill/core/browser/country_type.h"
+#include "components/autofill/core/browser/data_manager/payments/test_payments_data_manager.h"
+#include "components/autofill/core/browser/data_manager/test_personal_data_manager.h"
 #include "components/autofill/core/browser/data_model/addresses/autofill_profile.h"
 #include "components/autofill/core/browser/data_model/payments/credit_card.h"
 #include "components/autofill/core/browser/field_types.h"
@@ -2088,6 +2090,43 @@
   EXPECT_EQ(initial_fill_id, refill_fill_id);
 }
 
+// Tests that refills skip sensitive fields if the initial fill did not include
+// them.
+TEST_F(FormFillerTest, RefillSkipsSensitiveFieldsIfNotFilledInitially) {
+  CreditCard credit_card = test::GetCreditCard();
+  // Create form with a credit card name by making one with a name and number,
+  // then removing the number field. It will be readded after initial fill.
+  FormData form =
+      test::GetFormData({.fields = {{.role = CREDIT_CARD_NAME_FULL,
+                                     .autocomplete_attribute = "cc-name"},
+                                    {.role = CREDIT_CARD_NUMBER,
+                                     .autocomplete_attribute = "cc-number"}}});
+  FormFieldData number_field = form.fields().back();
+  test_api(form).fields().pop_back();
+  FormsSeen({form});
+
+  // Initial fill.
+  form = AutofillForm(form, form.fields().front(), &credit_card);
+
+  // Now add the number field back to trigger refill.
+  test_api(form).fields().push_back(std::move(number_field));
+
+  // Expect refill, but expect NO new fields to be filled because number field
+  // is sensitive and was not filled initially.
+  EXPECT_CALL(autofill_driver(), ApplyFormAction)
+      .WillOnce([&](mojom::FormActionType action_type,
+                    mojom::ActionPersistence action_persistence,
+                    base::span<const FormFieldData> data, const FillId& fill_id,
+                    bool supports_refill, const url::Origin& triggered_origin,
+                    const absl::flat_hash_map<FieldGlobalId, FieldType>&,
+                    const Section&) {
+        EXPECT_TRUE(data.empty());
+        return std::vector<FieldGlobalId>{};
+      });
+
+  FormsSeen({form});
+}
+
 // Tests that a programmatic refill can be triggered within the timeout.
 // Also tests that a second refill within the timeout is a no-op.
 TEST_F(FormFillerTest, ProgrammaticRefillBeforeTimeout) {
Loading diff…

Original Bug Report

reported by [email protected]

Potential bypass of Payments Mandatory Reauth via Autofill refill mechanism for local cards

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: A potential logic flaw in the Autofill refill mechanism allows a malicious website to bypass the ‘Payments Mandatory Reauth’ feature for local credit cards. By injecting a credit card number field after an initial autofill of non-sensitive fields, an attacker can trigger a form refill. This refill uses a cached, unauthenticated credit card object containing the cleartext PAN, exposing it without user verification.

Affected files:

  • components/autofill/core/browser/filling/form_filler.cc
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc
  • components/autofill/core/browser/filling/payments/field_filling_payments_util.cc

Estimated timestamp from git blame: 2026-03-05

Vulnerability Details

The “Payments Mandatory Reauth” feature (“Always verify when using autofill”) is intended to require device authentication before filling sensitive credit card data. However, a potential logic flaw exists in how the Autofill refill mechanism handles local credit cards.

When a user selects a local credit card to fill a form that only contains non-sensitive fields (e.g., cc-name), BrowserAutofillManager::ShouldFetchCreditCard evaluates to false. This correctly skips the device authentication prompt at that moment. However, the full CreditCard object—which for local cards intrinsically holds the cleartext Primary Account Number (PAN) in memory—is passed directly to FormFiller::FillOrPreviewForm and copied by value into a RefillContext to support dynamic form changes.

If the website subsequently mutates the DOM to inject a sensitive field like <input autocomplete="cc-number">, Autofill detects the form change and schedules a refill via FormFiller::TriggerRefill. This refill process re-enters the filling logic using the cached, unauthenticated CreditCard object. Crucially, it re-enters below the BrowserAutofillManager layer, completely bypassing the ShouldFetchCreditCard and FetchCreditCard checks. The newly injected cc-number field satisfies the refill security cross-fill check (because both cc-name and cc-number belong to FieldTypeGroup::kCreditCard) and is populated with the cleartext PAN.

Please note: These are potential steps and analysis based on source code review; our tooling has not executed a live Proof of Concept.

Potential Steps to Reproduce

  1. An attacker hosts a webpage with a form containing only <input autocomplete="cc-name"> and <input autocomplete="cc-exp">.
  2. A user with “Always verify when using autofill” enabled and a saved local credit card visits the page.
  3. The user interacts with the cc-name field and selects their card from the Autofill dropdown. No biometric prompt is shown because no sensitive fields are present initially.
  4. Immediately after the initial fill, the attacker’s script synchronously injects a hidden <input autocomplete="cc-number"> into the DOM.
  5. Chrome’s renderer detects the mutation and triggers an automatic refill (RefillTriggerReason::kFormChanged).
  6. The browser process executes the refill using the cached RefillContext, populating the new cc-number field with the cleartext PAN without prompting the user for authentication.
  7. The attacker’s script reads the value of the injected input to steal the PAN.

Suggested Fix

There are two primary ways to address this:

  1. Sanitize the Cached Context: Ensure that the CreditCard object cached in the RefillContext has its sensitive data (PAN/CVC) stripped or masked if device authentication was not successfully completed during the initial fill.
  2. Re-evaluate Authentication on Refill: Modify FormFiller::TriggerRefill or FormFiller::FillOrPreviewForm to re-evaluate ShouldFetchCreditCard for the newly discovered fields. If an unauthenticated refill attempts to fill a CREDIT_CARD_NUMBER or CREDIT_CARD_VERIFICATION_CODE, it should either trigger the full CreditCardAccessManager::FetchCreditCard authentication flow or safely abort filling those specific sensitive fields.

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