Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Passwords
DescriptionInsufficient validation of untrusted input in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker513730012
Fix commitb817abd363e2 (chromium/src) +101/-46
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
for
components/password_manager/core/browser/field_info_manager.cc
modified
if
components/password_manager/core/browser/field_info_manager.cc
modified

Files Changed

  • chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
  • components/password_manager/core/browser/field_info_manager.cc
  • components/password_manager/core/browser/field_info_manager.h
  • components/password_manager/core/browser/field_info_manager_unittest.cc
  • components/password_manager/core/browser/password_form_manager.cc
  • components/password_manager/core/browser/password_form_manager.h
From b817abd363e2d63a396a9cecbf37f66cde5002fe Mon Sep 17 00:00:00 2001
From: Maria Kazinova <kazinova@google.com>
Date: Wed, 20 May 2026 05:59:40 -0700
Subject: [PATCH] [Passwords] Refactor server predictions storage in PasswordManager

This CL refactors `server_predictions_` in `PasswordManager` to be keyed by a pair of `autofill::FormSignature` and `driver_id` (integer ID of `PasswordManagerDriver`).
Previously, predictions were keyed only by `FormSignature`. This could lead to issues if multiple forms with the same signature appeared in different frames (e.g., cross-origin iframes), as their predictions in the shared map could overwrite each other or be adopted by the wrong frame.

Fixed: 513730012
Change-Id: Ie3a4be4556dee3d7782b35138131bddd4747f336
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852463
Commit-Queue: Maria Kazinova <kazinova@google.com>
Reviewed-by: Ioana Treib <ioanap@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633535}
---

diff --git a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
index 03351ebe..1a5d3125 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
@@ -784,10 +784,15 @@
       Observer::FieldTypeSource::kAutofillServer,
       /*small_forms_were_parsed=*/false);
 
+  ContentPasswordManagerDriver* password_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(main_rfh());
+  int driver_id = password_driver->GetId();
+
   EXPECT_THAT(static_cast<const password_manager::PasswordManager*>(
                   GetClient()->GetPasswordManager())
                   ->GetServerPredictionsForTesting(),
-              UnorderedElementsAre(Key(CalculateFormSignature(form))));
+              UnorderedElementsAre(
+                  Key(testing::Pair(CalculateFormSignature(form), driver_id))));
 }
 
 TEST_F(ChromePasswordManagerClientTest,
@@ -898,11 +903,21 @@
   // Even though `OnFieldTypesDetermined` was only called for a single form (the
   // browser form that is the result of merging both forms), password manager
   // receives predictions for both the main and the child form.
-  EXPECT_THAT(static_cast<const password_manager::PasswordManager*>(
-                  GetClient()->GetPasswordManager())
-                  ->GetServerPredictionsForTesting(),
-              UnorderedElementsAre(Key(CalculateFormSignature(main_form)),
-                                   Key(CalculateFormSignature(child_form))));
+  ContentPasswordManagerDriver* main_password_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(main_rfh());
+  ContentPasswordManagerDriver* child_password_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(child_rfh);
+  int main_driver_id = main_password_driver->GetId();
+  int child_driver_id = child_password_driver->GetId();
+
+  EXPECT_THAT(
+      static_cast<const password_manager::PasswordManager*>(
+          GetClient()->GetPasswordManager())
+          ->GetServerPredictionsForTesting(),
+      UnorderedElementsAre(
+          Key(testing::Pair(CalculateFormSignature(main_form), main_driver_id)),
+          Key(testing::Pair(CalculateFormSignature(child_form),
+                            child_driver_id))));
 }
 
 TEST_F(ChromePasswordManagerClientTest,
diff --git a/components/password_manager/core/browser/field_info_manager.cc b/components/password_manager/core/browser/field_info_manager.cc
index 1490145..31567668 100644
--- a/components/password_manager/core/browser/field_info_manager.cc
+++ b/components/password_manager/core/browser/field_info_manager.cc
@@ -116,7 +116,8 @@
 }
 
 void FieldInfoManager::ProcessServerPredictions(
-    const std::map<autofill::FormSignature, FormPredictions>& predictions) {
+    const std::map<std::pair<autofill::FormSignature, int>, FormPredictions>&
+        predictions) {
   for (auto& entry : field_info_cache_) {
     FieldInfo& field_info = entry.field_info;
     // Do nothing if predictions are already stored.
@@ -124,12 +125,12 @@
       continue;
     }
 
-    for (const auto& prediction : predictions) {
-      // Do nothing if drivers do not match.
-      if (field_info.driver_id != prediction.second.driver_id) {
+    for (const auto& [key, form_predictions] : predictions) {
+      const int driver_id = key.second;
+      if (driver_id != field_info.driver_id) {
         continue;
       }
-      if (StoresPredictionsForInfo(prediction.second, field_info)) {
+      if (StoresPredictionsForInfo(form_predictions, field_info)) {
         break;
       }
     }
diff --git a/components/password_manager/core/browser/field_info_manager.h b/components/password_manager/core/browser/field_info_manager.h
index 0572787..c1fc020 100644
--- a/components/password_manager/core/browser/field_info_manager.h
+++ b/components/password_manager/core/browser/field_info_manager.h
@@ -78,7 +78,8 @@
 
   // Propagates signatures and field type received from the server.
   void ProcessServerPredictions(
-      const std::map<autofill::FormSignature, FormPredictions>& predictions);
+      const std::map<std::pair<autofill::FormSignature, int>, FormPredictions>&
+          predictions);
 
  private:
   struct FieldInfoEntry {
diff --git a/components/password_manager/core/browser/field_info_manager_unittest.cc b/components/password_manager/core/browser/field_info_manager_unittest.cc
index 7b8d9d2..81f1e53 100644
--- a/components/password_manager/core/browser/field_info_manager_unittest.cc
+++ b/components/password_manager/core/browser/field_info_manager_unittest.cc
@@ -140,7 +140,8 @@
   manager_->AddFieldInfo(info, /*predictions=*/std::nullopt);
 
   // Create test predictions.
-  std::map<autofill::FormSignature, FormPredictions> predictions;
+  std::map<std::pair<autofill::FormSignature, int>, FormPredictions>
+      predictions;
   FormPredictions form_prediction =
       CreateTestPredictions(kTestDriverId, kTestFormSignature,
                             kTestFieldSignature, kTestFieldId, kTestFieldType);
@@ -149,13 +150,14 @@
   form_prediction.fields.emplace_back(kAnotherFieldId, kAnotherFieldSignature,
                                       kAnotherFieldType, /*is_override=*/false);
 
-  predictions[kTestFormSignature] = form_prediction;
+  predictions[{kTestFormSignature, kTestDriverId}] = form_prediction;
 
   // Add a prediction with the same field id, but different driver.
   FormPredictions different_driver_prediction = CreateTestPredictions(
       kAnotherDriverId, kAnotherFormSignature, kAnotherFieldSignature,
       kTestFieldId, kAnotherFieldType);
-  predictions[kAnotherFormSignature] = different_driver_prediction;
+  predictions[{kAnotherFormSignature, kAnotherDriverId}] =
+      different_driver_prediction;
 
   manager_->ProcessServerPredictions(predictions);
 
diff --git a/components/password_manager/core/browser/password_form_manager.cc b/components/password_manager/core/browser/password_form_manager.cc
index 06e725b5..dcfa96a9 100644
--- a/components/password_manager/core/browser/password_form_manager.cc
+++ b/components/password_manager/core/browser/password_form_manager.cc
@@ -1145,7 +1145,8 @@
 }
 
 void PasswordFormManager::ProcessServerPredictions(
-    const std::map<FormSignature, FormPredictions>& predictions) {
+    const std::map<std::pair<autofill::FormSignature, int>, FormPredictions>&
+        predictions) {
   if (parser_.server_predictions()) {
     // This method might be called multiple times. No need to process
     // predictions again.
@@ -1589,7 +1590,8 @@
 }
 
 void PasswordFormManager::UpdateServerPredictionsForObservedForm(
-    const std::map<FormSignature, FormPredictions>& predictions) {
+    const std::map<std::pair<autofill::FormSignature, int>, FormPredictions>&
+        predictions) {
   CHECK(observed_form());
   if (net::IsLocalhost(observed_form()->url())) {
     // Avoid relying on crowdsourcing on localhost to avoid aggregating multiple
@@ -1601,7 +1603,7 @@
 
   FormSignature observed_form_signature =
       CalculateFormSignature(*observed_form());
-  auto it = predictions.find(observed_form_signature);
+  auto it = predictions.find({observed_form_signature, driver_id_});
   if (it == predictions.end()) {
     return;
   }
@@ -1633,7 +1635,8 @@
 
 void PasswordFormManager::UpdateFormManagerWithFormChanges(
     const FormData& observed_form_data,
-    const std::map<FormSignature, FormPredictions>& predictions) {
+    const std::map<std::pair<autofill::FormSignature, int>, FormPredictions>&
+        predictions) {
   *mutable_observed_form() = observed_form_data;
 
   // If the observed form has changed, it might be autofilled again.
diff --git a/components/password_manager/core/browser/password_form_manager.h b/components/password_manager/core/browser/password_form_manager.h
index 213ecb6..9ee45ec3 100644
--- a/components/password_manager/core/browser/password_form_manager.h
+++ b/components/password_manager/core/browser/password_form_manager.h
@@ -166,7 +166,8 @@
   // |observed_form()|, initiates filling and stores predictions in
   // |predictions_|.
   void ProcessServerPredictions(
-      const std::map<autofill::FormSignature, FormPredictions>& predictions);
+      const std::map<std::pair<autofill::FormSignature, int>, FormPredictions>&
+          predictions);
 
   // Stores model predictions in the `parser_`.
   void ProcessModelPredictions(
@@ -182,7 +183,8 @@
   // for server predictions.
   void UpdateFormManagerWithFormChanges(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
index 03351ebe..1a5d3125 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
@@ -784,10 +784,15 @@
       Observer::FieldTypeSource::kAutofillServer,
       /*small_forms_were_parsed=*/false);
 
+  ContentPasswordManagerDriver* password_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(main_rfh());
+  int driver_id = password_driver->GetId();
+
   EXPECT_THAT(static_cast<const password_manager::PasswordManager*>(
                   GetClient()->GetPasswordManager())
                   ->GetServerPredictionsForTesting(),
-              UnorderedElementsAre(Key(CalculateFormSignature(form))));
+              UnorderedElementsAre(
+                  Key(testing::Pair(CalculateFormSignature(form), driver_id))));
 }
 
 TEST_F(ChromePasswordManagerClientTest,
@@ -898,11 +903,21 @@
   // Even though `OnFieldTypesDetermined` was only called for a single form (the
   // browser form that is the result of merging both forms), password manager
   // receives predictions for both the main and the child form.
-  EXPECT_THAT(static_cast<const password_manager::PasswordManager*>(
-                  GetClient()->GetPasswordManager())
-                  ->GetServerPredictionsForTesting(),
-              UnorderedElementsAre(Key(CalculateFormSignature(main_form)),
-                                   Key(CalculateFormSignature(child_form))));
+  ContentPasswordManagerDriver* main_password_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(main_rfh());
+  ContentPasswordManagerDriver* child_password_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(child_rfh);
+  int main_driver_id = main_password_driver->GetId();
+  int child_driver_id = child_password_driver->GetId();
+
+  EXPECT_THAT(
+      static_cast<const password_manager::PasswordManager*>(
+          GetClient()->GetPasswordManager())
+          ->GetServerPredictionsForTesting(),
+      UnorderedElementsAre(
+          Key(testing::Pair(CalculateFormSignature(main_form), main_driver_id)),
+          Key(testing::Pair(CalculateFormSignature(child_form),
+                            child_driver_id))));
 }
 
 TEST_F(ChromePasswordManagerClientTest,
diff --git a/components/password_manager/core/browser/field_info_manager_unittest.cc b/components/password_manager/core/browser/field_info_manager_unittest.cc
index 7b8d9d2..81f1e53 100644
--- a/components/password_manager/core/browser/field_info_manager_unittest.cc
+++ b/components/password_manager/core/browser/field_info_manager_unittest.cc
@@ -140,7 +140,8 @@
   manager_->AddFieldInfo(info, /*predictions=*/std::nullopt);
 
   // Create test predictions.
-  std::map<autofill::FormSignature, FormPredictions> predictions;
+  std::map<std::pair<autofill::FormSignature, int>, FormPredictions>
+      predictions;
   FormPredictions form_prediction =
       CreateTestPredictions(kTestDriverId, kTestFormSignature,
                             kTestFieldSignature, kTestFieldId, kTestFieldType);
@@ -149,13 +150,14 @@
   form_prediction.fields.emplace_back(kAnotherFieldId, kAnotherFieldSignature,
                                       kAnotherFieldType, /*is_override=*/false);
 
-  predictions[kTestFormSignature] = form_prediction;
+  predictions[{kTestFormSignature, kTestDriverId}] = form_prediction;
 
   // Add a prediction with the same field id, but different driver.
   FormPredictions different_driver_prediction = CreateTestPredictions(
       kAnotherDriverId, kAnotherFormSignature, kAnotherFieldSignature,
       kTestFieldId, kAnotherFieldType);
-  predictions[kAnotherFormSignature] = different_driver_prediction;
+  predictions[{kAnotherFormSignature, kAnotherDriverId}] =
+      different_driver_prediction;
 
   manager_->ProcessServerPredictions(predictions);
diff --git a/components/password_manager/core/browser/password_form_manager_unittest.cc b/components/password_manager/core/browser/password_form_manager_unittest.cc
index fa79a6f..5b4c3408 100644
--- a/components/password_manager/core/browser/password_form_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_form_manager_unittest.cc
@@ -210,6 +210,7 @@
               GetLastCommittedOrigin,
               (),
               (const, override));
+  MOCK_METHOD(int, GetId, (), (const, override));
 };
 
 class MockPasswordManagerClient : public StubPasswordManagerClient {
@@ -330,11 +331,13 @@
 }
 
 // Create predictions for |form| using field predictions |field_predictions|.
-std::map<FormSignature, FormPredictions> CreatePredictions(
-    const FormData& form,
-    std::vector<std::pair<int, FieldType>> field_predictions,
-    bool is_override = false) {
+std::map<std::pair<autofill::FormSignature, int>, FormPredictions>
+CreatePredictions(const FormData& form,
+                  std::vector<std::pair<int, FieldType>> field_predictions,
+                  bool is_override = false,
+                  int driver_id = 0) {
   FormPredictions predictions;
+  predictions.driver_id = driver_id;
   for (const auto& index_prediction : field_predictions) {
     autofill::FieldRendererId renderer_id =
         form.fields()[index_prediction.first].renderer_id();
@@ -344,8 +347,9 @@
     predictions.fields.emplace_back(renderer_id, field_signature, server_type,
                                     is_override);
   }
-  FormSignature form_signature = CalculateFormSignature(form);
-  return {{form_signature, predictions}};
+  autofill::FormSignature form_signature =
+      autofill::CalculateFormSignature(form);
+  return {{{form_signature, driver_id}, predictions}};
 }
 
 // Create simple predictions on single username field.
@@ -989,8 +993,8 @@
   SetNonFederatedAndNotifyFetchCompleted({saved_match_});
   Mock::VerifyAndClearExpectations(&driver_);
 
-  std::map<FormSignature, FormPredictions> predictions = CreatePredictions(
-      observed_form_, {std::make_pair(2, autofill::PASSWORD)});
+  auto predictions = CreatePredictions(observed_form_,
+                                       {std::make_pair(2, autofill::PASSWORD)});
 
   // Expect filling without delay on receiving server predictions.
   EXPECT_CALL(driver_, PropagateFillDataOnParsingCompletion).Times(1);
@@ -1002,6 +1006,31 @@
   form_manager_->ProcessServerPredictions(predictions);
 }
 
+// Tests that PasswordFormManager does not adopt server predictions if they
+// belong to a different driver.
+TEST_P(PasswordFormManagerTest, ServerPredictionsDriverIdMismatch) {
+  EXPECT_CALL(driver_, GetId()).WillRepeatedly(Return(1));
+  CreateFormManager(observed_form_);
+
+  // Expects no filling on save matches receiving.
+  EXPECT_CALL(driver_, PropagateFillDataOnParsingCompletion).Times(0);
+  SetNonFederatedAndNotifyFetchCompleted({saved_match_});
+  Mock::VerifyAndClearExpectations(&driver_);
+
+  auto base_predictions = CreatePredictions(
+      observed_form_, {std::make_pair(2, autofill::PASSWORD)});
+
+  std::map<std::pair<autofill::FormSignature, int>, FormPredictions>
+      predictions;
+  predictions[{autofill::CalculateFormSignature(observed_form_), 2}] =
+      base_predictions.begin()->second;
+
+  // Expect NO filling on receiving server predictions because driver_id
+  // mismatches.
+  EXPECT_CALL(driver_, PropagateFillDataOnParsingCompletion).Times(0);
+  form_manager_->ProcessServerPredictions(predictions);
+}
+
 // Tests that PasswordFormManager fills after some delay even without
 // server predictions.
 TEST_P(PasswordFormManagerTest, ServerPredictionsAfterDelay) {
@@ -1013,8 +1042,8 @@
   task_environment_.FastForwardUntilNoTasksRemain();
   Mock::VerifyAndClearExpectations(&driver_);
 
-  std::map<FormSignature, FormPredictions> predictions = CreatePredictions(
-      observed_form_, {std::make_pair(2, autofill::PASSWORD)});
+  auto predictions = CreatePredictions(observed_form_,
+                                       {std::make_pair(2, autofill::PASSWORD)});
 
   // Expect filling on receiving server predictions because it was less than
   // kMaxTimesAutofill attempts to fill.
@@ -1031,8 +1060,8 @@
   EXPECT_CALL(driver_, PropagateFillDataOnParsingCompletion).Times(0);
   CreateFormManager(observed_form_);
 
-  std::map<FormSignature, FormPredictions> predictions = CreatePredictions(
-      observed_form_, {std::make_pair(2, autofill::PASSWORD)});
+  auto predictions = CreatePredictions(observed_form_,
+                                       {std::make_pair(2, autofill::PASSWORD)});
   form_manager_->ProcessServerPredictions(predictions);
   Mock::VerifyAndClearExpectations(&driver_);
 
@@ -1193,7 +1222,7 @@
   test_api(anonymous_signup).field(2).set_name({});
   test_api(anonymous_signup).field(2).set_value(u"a password");
   // Mark the password field as new-password.
-  std::map<FormSignature, FormPredictions> predictions = CreatePredictions(
+  auto predictions = CreatePredictions(
       observed_form_, {std::make_pair(2, autofill::ACCOUNT_CREATION_PASSWORD)});
 
   form_manager_->ProcessServerPredictions(predictions);
@@ -3793,7 +3822,7 @@
 
   // Provide server predictions for the single username form, which will trigger
   // FillNow().
-  std::map<FormSignature, FormPredictions> predictions =
+  auto predictions =
       CreatePredictions(non_password_form_,
                         {std::make_pair(kUsernameFieldIndex, SINGLE_USERNAME)});
   form_manager_->ProcessServerPredictions(predictions);
@@ -4208,7 +4237,7 @@
 // not provisionally saved.
 TEST_P(PasswordFormManagerTest, ProvisinallySavedOnSingleUsernameForm) {
   CreateFormManager(non_password_form_);
-  std::map<FormSignature, FormPredictions> predictions =
+  auto predictions =
       CreatePredictions(non_password_form_,
                         {std::make_pair(kUsernameFieldIndex, SINGLE_USERNAME)});
   form_manager_->ProcessServerPredictions(predictions);
@@ -4667,7 +4696,7 @@
 
   // Expect no filling on receiving predictions.
   EXPECT_CALL(driver_, PropagateFillDataOnParsingCompletion).Times(0);
-  form_manager_->ProcessServerPredictions({{kFormSignature, predictions}});
+  form_manager_->ProcessServerPredictions({{{kFormSignature, 0}, predictions}});
 }
 
 // Tests that crowdsourcing votes are not uploaded for forms on localhost.
@@ -4850,7 +4879,7 @@
   SetNonFederatedAndNotifyFetchCompleted({saved_match_});
 
   // Server prediction marks element on index three as a credit card field.
-  std::map<FormSignature, FormPredictions> predictions = CreatePredictions(
+  auto predictions = CreatePredictions(
       observed_form_, {std::make_pair(3, autofill::CREDIT_CARD_NAME_FULL)});
 
   PasswordFormFillData fill_data;
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-origin server prediction poisoning in PasswordManager via FormSignature collision

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: A malicious cross-origin subframe can potentially poison the shared Autofill server predictions map in the PasswordManager by reporting a form with a spoofed action URL that generates a colliding FormSignature. This allows an attacker to manipulate field-type identification for a victim origin’s form within the same tab. The impact includes functional disruption of autofill and the potential for a user’s password to be saved as a username in the browser’s UI.

Affected files:

  • components/password_manager/core/browser/password_manager.cc
  • components/password_manager/core/browser/password_form_manager.cc
  • components/password_manager/core/browser/form_parsing/form_data_parser.cc
  • components/password_manager/core/browser/password_manager.h

Estimated timestamp from git blame: 2018-11-16

Summary

A potential vulnerability exists in the PasswordManager where Autofill server predictions are stored in a shared map (one per WebContents) keyed by FormSignature without sufficient validation of the source frame’s origin. Because FormSignature is calculated using renderer-provided data—specifically the form’s action URL—that is not validated against the frame’s origin in the browser, a cross-origin subframe can inject malicious predictions into this map. If the subframe’s injected FormSignature matches that of a form in another frame (e.g., the main frame), the victim’s PasswordFormManager may adopt these poisoned predictions.

Root Cause Analysis

The PasswordManager maintains a map of Autofill server predictions:

// components/password_manager/core/browser/password_manager.h
std::map<autofill::FormSignature, FormPredictions> server_predictions_;

Predictions are updated in PasswordManager::ProcessAutofillPredictions. The key, FormSignature, is generated from FormData. While the browser process sanitizes FormData::url in ContentAutofillDriver::Lift, it does not validate or overwrite the action URL provided by the renderer. Consequently, a subframe can report a form claiming an action URL belonging to a different origin.

When a PasswordFormManager for a victim frame retrieves predictions, it uses the FormSignature to look up the shared map. The matching logic in PasswordFormManager::UpdateServerPredictionsForObservedForm relies on FieldRendererId to map predictions to fields. Because FieldRendererId values are sequential and highly predictable across processes (often starting from 1), an attacker can easily craft a form whose IDs collide with the victim’s form.

Potential Impact

By poisoning the server_predictions_ map, an attacker can steer the FormDataParser logic in the victim frame:

  1. Field-Role Swapping: An attacker can map a USERNAME prediction to the victim’s password field’s renderer_id. This causes the user’s plaintext password to be saved as the ‘username’ in the browser’s password manager, exposing it in the save bubble and subsequent autofill UI.
  2. Field Suppression: An attacker can map a NOT_PASSWORD prediction to a password field, effectively disabling the Password Manager for that form.

Suggested/Potential Reproduction Steps

  1. A user navigates to https://victim.com/login which contains a standard login form (username ID 1, password ID 2) and an attacker-controlled iframe https://attacker.com.
  2. The iframe at attacker.com reports a form via mojom::AutofillDriver::FormsSeen with its action URL set to https://victim.com/login and field names matching the victim’s form.
  3. The attacker swaps the FieldRendererIds in their report: they assign ID 1 to the ‘username’ field and ID 2 to the ‘password’ field, but instruct the Autofill server (through crowdsourcing) to treat ID 2 as the username.
  4. Once the server predictions arrive, the shared server_predictions_ map is updated with the poisoned entry for the victim’s FormSignature.
  5. When the user interacts with the victim’s form, PasswordFormManager adopts the poisoned mapping: {ID 2 -> USERNAME}.
  6. Upon submission, the Password Manager identifies the field with ID 2 (the actual password field) as the username and offers to save it in plaintext.

The PasswordFormManager::UpdateServerPredictionsForObservedForm should verify that the predictions in the shared map were generated by a frame with a matching origin or driver_id before adopting them. Additionally, the browser’s ContentAutofillDriver::Lift should validate the action URL against the frame’s origin.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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