CVE-2026-11251
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pcomponents/password_manager/core/browser/password_manager_unittest.cc |
modified |
Files Changed
components/password_manager/core/browser/password_manager.cccomponents/password_manager/core/browser/password_manager_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccchrome/browser/password_manager/chrome_password_manager_client.cccomponents/password_manager/core/browser/password_form_manager.cccomponents/password_manager/core/browser/password_save_manager_impl.cccomponents/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.
- Launch Chrome with a clean profile and open an Incognito window.
- Navigate to an attacker-controlled site (e.g.,
https://attacker.com). - Assume the attacker gains Remote Code Execution (RCE) within the renderer process for their site.
- The compromised renderer constructs a synthetic
autofill::FormDatarepresenting a login form and sends theautofill::mojom::PasswordManagerDriver::PasswordFormsParsedIPC to the browser process. - The browser sanitizes the form to match the
attacker.comorigin and caches it in aPasswordFormManager. - The compromised renderer then requests the
autofill::mojom::PasswordGenerationDriverMojo interface and sends thePresaveGeneratedPasswordIPC with the same form data and an attacker-controlled password string. - The browser process receives the IPC, bypasses the compiled-out
DCHECK(client_->IsSavingAndFillingEnabled(...)), and proceeds to save the credential. - Close the Incognito window.
- Open
chrome://password-managerin the regular profile. The injected credential forhttps://attacker.comwill 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.