Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Password Manager
DescriptionInsufficient validation of untrusted input in Password Manager
ComponentPassword Manager
Bug ClassLogic Error
Tracker498301853
Fix commitf25dc3f6f416 (chromium/src) +22/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_P
components/password_manager/core/browser/password_manager_unittest.cc
modified

Files Changed

  • components/password_manager/core/browser/password_manager.cc
  • components/password_manager/core/browser/password_manager_unittest.cc
From f25dc3f6f416b1826c3547b3a82bc0c7ee722eda Mon Sep 17 00:00:00 2001
From: Vasilii Sukhanov <[email protected]>
Date: Tue, 07 Apr 2026 07:00:06 -0700
Subject: [PATCH] Fix potential Incognito and policy bypass via DCHECK in OnPresaveGeneratedPassword

Replace DCHECK with a runtime check in OnPresaveGeneratedPassword to
prevent saving passwords when saving is disabled. Add a unit test to
verify this behavior.

Fixed: 498301853
Change-Id: I45d0edd67bb5ae032f3064a0a4297c2d7cfac987
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735722
Commit-Queue: Vasilii Sukhanov <[email protected]>
Reviewed-by: Viktor Semeniuk <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610718}
---

diff --git a/components/password_manager/core/browser/password_manager.cc b/components/password_manager/core/browser/password_manager.cc
index 80d132e..5a9f0fd 100644
--- a/components/password_manager/core/browser/password_manager.cc
+++ b/components/password_manager/core/browser/password_manager.cc
@@ -722,7 +722,9 @@
     PasswordManagerDriver* driver,
     const FormData& form_data,
     const std::u16string& generated_password) {
-  DCHECK(client_->IsSavingAndFillingEnabled(form_data.url()));
+  if (!client_->IsSavingAndFillingEnabled(form_data.url())) {
+    return;
+  }
   PasswordFormManager* form_manager =
       GetMatchedManagerForForm(driver, form_data.renderer_id());
   UMA_HISTOGRAM_BOOLEAN("PasswordManager.GeneratedFormHasNoFormManager",
diff --git a/components/password_manager/core/browser/password_manager_unittest.cc b/components/password_manager/core/browser/password_manager_unittest.cc
index abb4ae5c..bd6c456 100644
--- a/components/password_manager/core/browser/password_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_manager_unittest.cc
@@ -955,6 +955,25 @@
   EXPECT_EQ(forms_saved[0].password_value, generated_password);
 }
 
+TEST_P(PasswordManagerTest, GeneratedPasswordFormSubmit_SavingDisabled) {
+  EXPECT_CALL(client_, IsSavingAndFillingEnabled).WillRepeatedly(Return(false));
+
+  std::vector<FormData> observed;
+  FormData form_data(MakeSignUpFormData());
+  observed.push_back(form_data);
+  manager()->OnPasswordFormsParsed(&driver_, observed);
+  manager()->OnPasswordFormsRendered(&driver_, observed);
+
+  // Simulate the user generating the password.
+  const std::u16string generated_password = u"GeNeRaTeDRaNdOmPa$$";
+  manager()->OnPresaveGeneratedPassword(&driver_, form_data,
+                                        generated_password);
+  task_environment_.RunUntilIdle();
+
+  // Verify that it was NOT saved!
+  EXPECT_THAT(store_->stored_passwords(), IsEmpty());
+}
+
 #if BUILDFLAG(IS_IOS)
 
 // Tests that the information held by the field data manager is propagated on
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/password_manager/core/browser/password_manager_unittest.cc b/components/password_manager/core/browser/password_manager_unittest.cc
index abb4ae5c..bd6c456 100644
--- a/components/password_manager/core/browser/password_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_manager_unittest.cc
@@ -955,6 +955,25 @@
   EXPECT_EQ(forms_saved[0].password_value, generated_password);
 }
 
+TEST_P(PasswordManagerTest, GeneratedPasswordFormSubmit_SavingDisabled) {
+  EXPECT_CALL(client_, IsSavingAndFillingEnabled).WillRepeatedly(Return(false));
+
+  std::vector<FormData> observed;
+  FormData form_data(MakeSignUpFormData());
+  observed.push_back(form_data);
+  manager()->OnPasswordFormsParsed(&driver_, observed);
+  manager()->OnPasswordFormsRendered(&driver_, observed);
+
+  // Simulate the user generating the password.
+  const std::u16string generated_password = u"GeNeRaTeDRaNdOmPa$$";
+  manager()->OnPresaveGeneratedPassword(&driver_, form_data,
+                                        generated_password);
+  task_environment_.RunUntilIdle();
+
+  // Verify that it was NOT saved!
+  EXPECT_THAT(store_->stored_passwords(), IsEmpty());
+}
+
 #if BUILDFLAG(IS_IOS)
 
 // Tests that the information held by the field data manager is propagated on
Loading diff…

Original Bug Report

reported by [email protected]

Incognito and policy bypass via DCHECK in OnPresaveGeneratedPassword

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can bypass Incognito and enterprise policy restrictions to save passwords for its own origin into the main profile. This occurs because PasswordManager::OnPresaveGeneratedPassword relies on a DCHECK rather than a runtime check to verify if password saving is enabled. In release builds, this check is compiled out, allowing the browser to persistently write the credentials to disk.

Affected files:

  • components/password_manager/core/browser/password_manager.cc
  • chrome/browser/password_manager/chrome_password_manager_client.cc
  • components/password_manager/core/browser/password_form_manager.cc
  • components/password_manager/core/browser/password_save_manager_impl.cc
  • components/password_manager/core/browser/form_saver_impl.cc

Estimated timestamp from git blame: 2024-05-27

Description

There is a potential logic flaw in the password manager that allows a compromised renderer process to bypass Incognito mode guarantees and enterprise policies (such as PasswordManagerEnabled = false) to persistently save credentials into the user’s main profile.

In components/password_manager/core/browser/password_manager.cc, the method OnPresaveGeneratedPassword relies on a DCHECK to ensure password saving is permitted:

void PasswordManager::OnPresaveGeneratedPassword(..., const FormData& form_data, ...) {
  DCHECK(client_->IsSavingAndFillingEnabled(form_data.url()));
  // ... proceeds to save the password
}

Because DCHECK macros evaluate to a no-op in official Release builds, this critical policy check is bypassed in production.

When this IPC is triggered in an Incognito session, the browser attempts to save the generated password. Because ChromePasswordManagerClient::GetProfilePasswordStore() requests the store using ServiceAccessType::EXPLICIT_ACCESS, the ProfilePasswordStoreFactory redirects the request and returns the PasswordStore of the original, non-Incognito profile. The underlying PasswordStore does not independently check if the active profile is Off-The-Record, resulting in the attacker-supplied credentials being persistently written to the main profile’s on-disk LoginDatabase.

The impact is constrained to the compromised renderer’s actual origin, as the browser securely sanitizes the incoming FormData using GetFormWithFrameAndFormMetaData(rfh) before passing it to the core PasswordManager.

Potential Steps to Reproduce

Note: These are suggested steps based on static analysis. Our tooling agent does not currently have the ability to run code to produce a working proof-of-concept.

  1. Launch Chrome with a clean profile and open an Incognito window.
  2. Navigate to an attacker-controlled site (e.g., https://attacker.com).
  3. Assume the attacker gains Remote Code Execution (RCE) within the renderer process for their site.
  4. The compromised renderer constructs a synthetic autofill::FormData representing a login form and sends the autofill::mojom::PasswordManagerDriver::PasswordFormsParsed IPC to the browser process.
  5. The browser sanitizes the form to match the attacker.com origin and caches it in a PasswordFormManager.
  6. The compromised renderer then requests the autofill::mojom::PasswordGenerationDriver Mojo interface and sends the PresaveGeneratedPassword IPC with the same form data and an attacker-controlled password string.
  7. The browser process receives the IPC, bypasses the compiled-out DCHECK(client_->IsSavingAndFillingEnabled(...)), and proceeds to save the credential.
  8. Close the Incognito window.
  9. Open chrome://password-manager in the regular profile. The injected credential for https://attacker.com will be persistently saved there.

Suggested Fix

Replace the DCHECK in PasswordManager::OnPresaveGeneratedPassword with a proper runtime check that safely drops the IPC if saving is disabled:

void PasswordManager::OnPresaveGeneratedPassword(
    PasswordManagerDriver* driver,
    const FormData& form_data,
    const std::u16string& generated_password) {
  if (!client_->IsSavingAndFillingEnabled(form_data.url())) {
    return;
  }
  // ...
}

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results from 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