Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Passwords
DescriptionMissing authorization in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker521502218
Fix commita08dd63d11f7 (chromium/src) +100/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
components/password_manager/core/browser/password_autofill_manager.cc
modified
TEST_F
components/password_manager/core/browser/password_autofill_manager_unittest.cc
modified

Files Changed

  • components/password_manager/content/browser/content_password_manager_driver.h
  • components/password_manager/core/browser/features/password_features.cc
  • components/password_manager/core/browser/features/password_features.h
  • components/password_manager/core/browser/password_autofill_manager.cc
  • components/password_manager/core/browser/password_autofill_manager_unittest.cc
  • components/password_manager/core/browser/password_manager_driver.h
From a08dd63d11f70a9f337b467d1b2dbc5657c50c58 Mon Sep 17 00:00:00 2001
From: Timofey Chudakov <[email protected]>
Date: Tue, 30 Jun 2026 01:14:53 -0700
Subject: [PATCH] [PWM][Security] Performs security checks in the pwm manual fallback.

This is a follow up to the crrev.com/c/7894835. The password manual
fallback flow entry is added only if the security checks pass for
the current frame. This CL adds the same checks to the manual
fallback flow to make sure a compromised frame can't trigger
manual fallback suggestions.

Fixed: 521502218
Change-Id: If6817c5169acd904623162fd885b96c5d404ad70
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8010689
Reviewed-by: Ioana Treib <[email protected]>
Commit-Queue: Timofey Chudakov <[email protected]>
Reviewed-by: Christoph Schwering <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1654631}
---

diff --git a/components/password_manager/content/browser/content_password_manager_driver.h b/components/password_manager/content/browser/content_password_manager_driver.h
index ca564ac..252d4622 100644
--- a/components/password_manager/content/browser/content_password_manager_driver.h
+++ b/components/password_manager/content/browser/content_password_manager_driver.h
@@ -55,11 +55,6 @@
       mojo::PendingAssociatedReceiver<autofill::mojom::PasswordManagerDriver>
           pending_receiver);
   void DidNavigate();
-  // Checks if the current URL is safe to share password data with. Kills the
-  // current renderer process if the URL is not safe and `may_kill_renderer` is
-  // `true`.
-  bool HasValidURL(bool may_kill_renderer = true);
-  bool IsRenderFrameHostSupported();
 
   // PasswordManagerDriver implementation.
   DriverId GetId() const override;
@@ -136,6 +131,8 @@
       const autofill::ParsingResult& parsing_result) override;
   void CheckViewAreaVisible(autofill::FieldRendererId field_id,
                             base::OnceCallback<void(bool)>) override;
+  bool HasValidURL(bool may_kill_renderer = true) override;
+  bool IsRenderFrameHostSupported() override;
   autofill::AutofillDriver* GetAutofillDriver() const override;
   base::WeakPtr<password_manager::PasswordManagerDriver> AsWeakPtr() override;
 
diff --git a/components/password_manager/core/browser/features/password_features.cc b/components/password_manager/core/browser/features/password_features.cc
index 033b436..e61e1f50 100644
--- a/components/password_manager/core/browser/features/password_features.cc
+++ b/components/password_manager/core/browser/features/password_features.cc
@@ -165,10 +165,8 @@
 
 BASE_FEATURE(kPasswordManagerLogToTerminal, base::FEATURE_DISABLED_BY_DEFAULT);
 
-#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)  // Desktop
 BASE_FEATURE(kPasswordManualFallbackSecurityChecks,
              base::FEATURE_DISABLED_BY_DEFAULT);
-#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
 
 BASE_FEATURE(kPasswordSaveInContextErrorResolution,
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/components/password_manager/core/browser/features/password_features.h b/components/password_manager/core/browser/features/password_features.h
index addb5546..9075ab0 100644
--- a/components/password_manager/core/browser/features/password_features.h
+++ b/components/password_manager/core/browser/features/password_features.h
@@ -176,11 +176,9 @@
 // terminal.
 BASE_DECLARE_FEATURE(kPasswordManagerLogToTerminal);
 
-#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)  // Desktop
 // Performs additional security checks wrt. the triggering frame before adding
 // the "Select password" entry to the context menu.
 BASE_DECLARE_FEATURE(kPasswordManualFallbackSecurityChecks);
-#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
 
 // When enabled, the user can resolve actionable errors in context during a
 // password save flow.
diff --git a/components/password_manager/core/browser/password_autofill_manager.cc b/components/password_manager/core/browser/password_autofill_manager.cc
index dcdeb92..62729c3a 100644
--- a/components/password_manager/core/browser/password_autofill_manager.cc
+++ b/components/password_manager/core/browser/password_autofill_manager.cc
@@ -555,6 +555,17 @@
   }
 #endif  // !BUILDFLAG(IS_ANDROID)
   if (autofill::IsPasswordsAutofillManuallyTriggered(field.trigger_source)) {
+    if (base::FeatureList::IsEnabled(
+            features::kPasswordManualFallbackSecurityChecks)) {
+      const bool manual_fallback_allowed_for_frame =
+          password_manager_driver_->HasValidURL() &&
+          password_manager_driver_->IsRenderFrameHostSupported();
+      if (!manual_fallback_allowed_for_frame) {
+        // Do not show manual fallback suggestions if the current frame doesn't
+        // meet security criteria, see crbug.com/521502218.
+        return;
+      }
+    }
     if (!manual_fallback_flow_) {
       manual_fallback_flow_ = std::make_unique<PasswordManualFallbackFlow>(
           password_manager_driver_, autofill_client_, password_client_,
diff --git a/components/password_manager/core/browser/password_autofill_manager_unittest.cc b/components/password_manager/core/browser/password_autofill_manager_unittest.cc
index f1fc6d36..4a8dfe4 100644
--- a/components/password_manager/core/browser/password_autofill_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_autofill_manager_unittest.cc
@@ -178,6 +178,8 @@
       (override));
   MOCK_METHOD(PasswordManager*, GetPasswordManager, (), (override));
   MOCK_METHOD(bool, CanShowAutofillUi, (), (const override));
+  MOCK_METHOD(bool, HasValidURL, (bool), (override));
+  MOCK_METHOD(bool, IsRenderFrameHostSupported, (), (override));
 
   gfx::RectF TransformToRootCoordinates(
       const gfx::RectF& bounds_in_frame_coordinates) override {
@@ -2080,9 +2082,16 @@
 }
 
 TEST_F(PasswordAutofillManagerTest, ManualFallback_InvokesFlow) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
   TestPasswordManagerClient client;
   InitializePasswordAutofillManager(&client,
                                     /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(true));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(true));
   autofill::TriggeringField field = kTriggeringField;
   field.trigger_source =
       autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
@@ -2093,10 +2102,59 @@
   password_autofill_manager_->ShowSuggestions(field);
 }
 
-TEST_F(PasswordAutofillManagerTest, ManualFallback_FlowResetOnNavigation) {
+TEST_F(PasswordAutofillManagerTest,
+       ManualFallback_UrlInsecure_NoManualFallback) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
   TestPasswordManagerClient client;
   InitializePasswordAutofillManager(&client,
                                     /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(false));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(true));
+  EXPECT_CALL(manual_fallback_flow(), RunFlow).Times(0);
+  autofill::TriggeringField field = kTriggeringField;
+  field.trigger_source =
+      autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
+  field.bounds = gfx::RectF(1, 1, 2, 2);
+  field.text_direction = base::i18n::LEFT_TO_RIGHT;
+  password_autofill_manager_->ShowSuggestions(field);
+}
+
+TEST_F(PasswordAutofillManagerTest,
+       ManualFallback_FrameHostNotSupported_NoManualFallback) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
+  TestPasswordManagerClient client;
+  InitializePasswordAutofillManager(&client,
+                                    /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(true));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(false));
+  EXPECT_CALL(manual_fallback_flow(), RunFlow).Times(0);
+  autofill::TriggeringField field = kTriggeringField;
+  field.trigger_source =
+      autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
+  field.bounds = gfx::RectF(1, 1, 2, 2);
+  field.text_direction = base::i18n::LEFT_TO_RIGHT;
+  password_autofill_manager_->ShowSuggestions(field);
+}
+
+TEST_F(PasswordAutofillManagerTest, ManualFallback_FlowResetOnNavigation) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
+  TestPasswordManagerClient client;
+  InitializePasswordAutofillManager(&client,
+                                    /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(true));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(true));
   autofill::TriggeringField field = kTriggeringField;
   field.trigger_source =
       autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
diff --git a/components/password_manager/core/browser/password_manager_driver.h b/components/password_manager/core/browser/password_manager_driver.h
index 98a9450..68aad492 100644
--- a/components/password_manager/core/browser/password_manager_driver.h
+++ b/components/password_manager/core/browser/password_manager_driver.h
@@ -243,6 +243,14 @@
   virtual void CheckViewAreaVisible(autofill::FieldRendererId field_id,
                                     base::OnceCallback<void(bool)>) = 0;
 
+  // Checks if the current URL is safe to share password data with. Kills the
+  // current renderer process if the URL is not safe and `may_kill_renderer` is
+  // `true`.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/password_manager/core/browser/password_autofill_manager_unittest.cc b/components/password_manager/core/browser/password_autofill_manager_unittest.cc
index f1fc6d36..4a8dfe4 100644
--- a/components/password_manager/core/browser/password_autofill_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_autofill_manager_unittest.cc
@@ -178,6 +178,8 @@
       (override));
   MOCK_METHOD(PasswordManager*, GetPasswordManager, (), (override));
   MOCK_METHOD(bool, CanShowAutofillUi, (), (const override));
+  MOCK_METHOD(bool, HasValidURL, (bool), (override));
+  MOCK_METHOD(bool, IsRenderFrameHostSupported, (), (override));
 
   gfx::RectF TransformToRootCoordinates(
       const gfx::RectF& bounds_in_frame_coordinates) override {
@@ -2080,9 +2082,16 @@
 }
 
 TEST_F(PasswordAutofillManagerTest, ManualFallback_InvokesFlow) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
   TestPasswordManagerClient client;
   InitializePasswordAutofillManager(&client,
                                     /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(true));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(true));
   autofill::TriggeringField field = kTriggeringField;
   field.trigger_source =
       autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
@@ -2093,10 +2102,59 @@
   password_autofill_manager_->ShowSuggestions(field);
 }
 
-TEST_F(PasswordAutofillManagerTest, ManualFallback_FlowResetOnNavigation) {
+TEST_F(PasswordAutofillManagerTest,
+       ManualFallback_UrlInsecure_NoManualFallback) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
   TestPasswordManagerClient client;
   InitializePasswordAutofillManager(&client,
                                     /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(false));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(true));
+  EXPECT_CALL(manual_fallback_flow(), RunFlow).Times(0);
+  autofill::TriggeringField field = kTriggeringField;
+  field.trigger_source =
+      autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
+  field.bounds = gfx::RectF(1, 1, 2, 2);
+  field.text_direction = base::i18n::LEFT_TO_RIGHT;
+  password_autofill_manager_->ShowSuggestions(field);
+}
+
+TEST_F(PasswordAutofillManagerTest,
+       ManualFallback_FrameHostNotSupported_NoManualFallback) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
+  TestPasswordManagerClient client;
+  InitializePasswordAutofillManager(&client,
+                                    /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(true));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(false));
+  EXPECT_CALL(manual_fallback_flow(), RunFlow).Times(0);
+  autofill::TriggeringField field = kTriggeringField;
+  field.trigger_source =
+      autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
+  field.bounds = gfx::RectF(1, 1, 2, 2);
+  field.text_direction = base::i18n::LEFT_TO_RIGHT;
+  password_autofill_manager_->ShowSuggestions(field);
+}
+
+TEST_F(PasswordAutofillManagerTest, ManualFallback_FlowResetOnNavigation) {
+  base::test::ScopedFeatureList security_checks_feature_list{
+      features::kPasswordManualFallbackSecurityChecks};
+  TestPasswordManagerClient client;
+  InitializePasswordAutofillManager(&client,
+                                    /*autofill_client=*/nullptr);
+
+  ON_CALL(*client.mock_driver(), HasValidURL(/*may_kill_renderer=*/true))
+      .WillByDefault(Return(true));
+  ON_CALL(*client.mock_driver(), IsRenderFrameHostSupported())
+      .WillByDefault(Return(true));
   autofill::TriggeringField field = kTriggeringField;
   field.trigger_source =
       autofill::AutofillSuggestionTriggerSource::kManualFallbackPasswords;
Loading diff…

Original Bug Report

reported by [email protected]

Bypass of password manual fallback security checks via direct mojom::AutofillDriver IPC

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 potential vulnerability in Chromium’s password manual fallback mechanism allows a compromised sandboxed iframe renderer to bypass origin restrictions and access saved credentials of its precursor domain. The security checks introduced via the kPasswordManualFallbackSecurityChecks flag are only enforced on the trusted context-menu building path and can be completely bypassed by directly invoking the mojom::AutofillDriver::AskForValuesToFill IPC interface. This could lead to a cross-origin credential disclosure from a process-isolated sandboxed iframe.

Affected files:

  • components/password_manager/core/browser/password_autofill_manager.cc
  • components/password_manager/core/browser/password_manual_fallback_flow.cc
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc
  • components/autofill/content/browser/content_autofill_driver.cc
  • chrome/browser/ui/autofill/autofill_context_menu_manager.cc

Estimated timestamp from git blame: 2024-04-08

Potential Root Cause

The security gate kPasswordManualFallbackSecurityChecks is designed to prevent manual fallback credential leakage inside untrusted environments. However, this gate is currently only enforced in exactly one trusted context-menu construction path:

chrome/browser/ui/autofill/autofill_context_menu_manager.cc:

if (base::FeatureList::IsEnabled(
        password_manager::features::kPasswordManualFallbackSecurityChecks) &&
    (!password_manager_driver.HasValidURL(/*may_kill_renderer*/ false) ||
     !password_manager_driver.IsRenderFrameHostSupported())) {
  return false;
}

A compromised renderer process can completely bypass the context menu builder and directly invoke the underlying browser-side Mojo IPC sink, mojom::AutofillDriver::AskForValuesToFill.

When a compromised renderer directly calls AskForValuesToFill with the trigger source set to kManualFallbackPasswords, the browser process routes the call as follows:

  1. ContentAutofillDriver::AskForValuesToFill (components/autofill/content/browser/content_autofill_driver.cc) accepts the call and runs bad_message::CheckArgs which permits kManualFallbackPasswords and only blocklists unrelated trigger sources.
  2. The request is routed via RouteToManager to BrowserAutofillManager::OnAskForValuesToFillImpl (components/autofill/core/browser/foundations/browser_autofill_manager.cc).
  3. The manager identifies the password request and delegates it to the sandboxed frame’s PasswordAutofillManager::ShowSuggestions (components/password_manager/core/browser/password_autofill_manager.cc).
  4. ShowSuggestions identifies the manual fallback trigger source and instantiates PasswordManualFallbackFlow (components/password_manager/core/browser/password_manual_fallback_flow.cc).
  5. The PasswordManualFallbackFlow constructor queries password_manager_driver_->GetLastCommittedURL() to obtain the realm for database lookup. For a sandboxed iframe with an opaque origin, this returns the HTTP/HTTPS precursor URL (e.g., https://victim.example/).
  6. FormFetcherImpl fetches the credentials matching https://victim.example/ from the user’s password database.
  7. When generating suggestions, because the matched credential’s realm matches the frame’s precursor URL, is_cross_domain evaluates to false in components/password_manager/core/browser/password_suggestion_generator.cc.
  8. The browser displays the trusted popup over the sandboxed iframe. If the root suggestion is marked unacceptable because the form cache is empty, the child suggestions (such as SuggestionType::kFillPassword) are still generated as acceptable.
  9. When the user clicks on the acceptable child suggestion, EnsureCrossDomainPasswordUsageGetsConsent skips the confirmation dialog (since is_cross_domain is false) and calls PasswordManagerDriver::FillField with the plaintext password payload.
  10. ContentPasswordManagerDriver::FillField (components/password_manager/content/browser/content_password_manager_driver.cc) lacks any HasValidURL() check and directly forwards the plaintext credential over the Mojo associated interface back to the compromised renderer’s PasswordAutofillAgent.

Potential Attack Steps

Note: Since our testing framework does not currently have the capability to run execution code, these are potential/suggested steps to demonstrate the vulnerability:

  1. Ensure kIsolateSandboxedIframes is enabled. A victim user has a saved credential for https://victim.example/ in their Chrome profile.
  2. The user visits a page that embeds a sandboxed iframe pointing to https://victim.example/ (e.g., <iframe sandbox="allow-scripts" src="https://victim.example/sandboxed.html">), which forces the frame to load in a process-isolated sandboxed renderer process.
  3. The attacker compromises this sandboxed renderer process.
  4. The compromised renderer directly crafts and sends a mojom::AutofillDriver::AskForValuesToFill IPC message with trigger_source and password_request.field.trigger_source set to AutofillSuggestionTriggerSource::kManualFallbackPasswords.
  5. The browser process fetches the credentials matching https://victim.example/ and presents the trusted manual fallback popup to the user.
  6. The user selects the child password-filling suggestion from the popup.
  7. The browser process, skipping the cross-domain consent warning, transmits the cleartext password back to the compromised renderer.

Suggested Fix

Enforce origin and URL security gates at the browser-side IPC entry point rather than exclusively at the context-menu builder. Specifically, validate the calling context’s URL and frame support within PasswordAutofillManager::ShowSuggestions or BrowserAutofillManager::OnAskForValuesToFillImpl. If the kPasswordManualFallbackSecurityChecks flag is enabled, reject manual fallback requests when !password_manager_driver.HasValidURL(/*may_kill_renderer=*/false) or !password_manager_driver.IsRenderFrameHostSupported() evaluates to true, thus shutting down the IPC-direct bypass route from sandboxed frames.

Evaluated with Chrome root at commit: 3947e01999a53d4e2382e39736cb79d79c7dffcf


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