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
Tracker516917065
Fix commit6dd5f7d6e56b (chromium/src) +235/-96
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/autofill/autofill_context_menu_manager.cc
modified

Files Changed

  • chrome/browser/ui/autofill/autofill_context_menu_manager.cc
  • chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc
From 6dd5f7d6e56b4ad7a68587b8fb9150c936f547b2 Mon Sep 17 00:00:00 2001
From: Timofey Chudakov <tchudakov@google.com>
Date: Mon, 08 Jun 2026 05:59:18 -0700
Subject: [PATCH] [PWM] Do security checks when adding pwd manual fallback entry.

See the document for more information: http://shortn/_EQH7slf4BJ

Bug: 516917065
Change-Id: I01df45fd0e3aecb96ea1a94656e53b9eafb5dcfa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7894835
Reviewed-by: Maria Kazinova <kazinova@google.com>
Commit-Queue: Timofey Chudakov <tchudakov@google.com>
Cr-Commit-Position: refs/heads/main@{#1643131}
---

diff --git a/chrome/browser/ui/autofill/autofill_context_menu_manager.cc b/chrome/browser/ui/autofill/autofill_context_menu_manager.cc
index 9c1e6d6..98454a2 100644
--- a/chrome/browser/ui/autofill/autofill_context_menu_manager.cc
+++ b/chrome/browser/ui/autofill/autofill_context_menu_manager.cc
@@ -387,7 +387,7 @@
 
   // Do not show password manager context menu options for input fields that
   // cannot be filled by the driver. See crbug.com/40061116.
-  if (password_manager_driver && password_manager_driver->CanShowAutofillUi()) {
+  if (password_manager_driver) {
     add_passwords_fallback =
         ShouldAddPasswordsManualFallbackItem(*password_manager_driver);
   }
@@ -447,11 +447,20 @@
 
 bool AutofillContextMenuManager::ShouldAddPasswordsManualFallbackItem(
     ContentPasswordManagerDriver& password_manager_driver) {
+  if (!password_manager_driver.CanShowAutofillUi()) {
+    return false;
+  }
   // Password suggestions should not be triggered on text areas.
   if (params_.form_control_type == blink::mojom::FormControlType::kTextArea) {
     return false;
   }
 
+  if (base::FeatureList::IsEnabled(
+          password_manager::features::kPasswordManualFallbackSecurityChecks) &&
+      (!password_manager_driver.HasValidURL(/*may_kill_renderer*/ false) ||
+       !password_manager_driver.IsRenderFrameHostSupported())) {
+    return false;
+  }
   return password_manager_driver.GetPasswordManager()
       ->GetClient()
       ->IsFillingEnabled(password_manager_driver.GetLastCommittedURL());
diff --git a/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc b/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc
index 437d939..01fcd5d 100644
--- a/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc
+++ b/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc
@@ -60,6 +60,7 @@
 #include "components/sync/test/test_sync_service.h"
 #include "components/user_manager/user_names.h"
 #include "content/public/browser/browser_context.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -75,6 +76,12 @@
 using ::testing::ElementsAre;
 using ::testing::IsEmpty;
 using ::testing::Not;
+using ::testing::Pointee;
+using ::testing::Property;
+
+testing::Matcher<const ui::SimpleMenuModel*> NoAutofillItemsAdded() {
+  return Pointee(Property(&ui::SimpleMenuModel::GetItemCount, 0));
+}
 
 // Checks if the context menu model contains any entries with plus address
 // manual fallback labels or command ids. `arg` must be of type
@@ -212,13 +219,7 @@
     ASSERT_TRUE(
         ui_test_utils::NavigateToURL(browser(), GURL("http://test.com")));
 
-    menu_model_ = std::make_unique<ui::SimpleMenuModel>(nullptr);
-    render_view_context_menu_ = std::make_unique<TestRenderViewContextMenu>(
-        *main_rfh(), content::ContextMenuParams());
-    render_view_context_menu_->Init();
-    autofill_context_menu_manager_ =
-        std::make_unique<AutofillContextMenuManager>(
-            render_view_context_menu_.get(), menu_model_.get());
+    CreateAutofillContextMenu(main_rfh());
     autofill_context_menu_manager()->set_params_for_testing(
         CreateContextMenuParams());
   }
@@ -264,6 +265,16 @@
     return autofill_context_menu_manager_.get();
   }
 
+  void CreateAutofillContextMenu(content::RenderFrameHost* rfh) {
+    menu_model_ = std::make_unique<ui::SimpleMenuModel>(nullptr);
+    render_view_context_menu_ = std::make_unique<TestRenderViewContextMenu>(
+        *rfh, content::ContextMenuParams());
+    render_view_context_menu_->Init();
+    autofill_context_menu_manager_ =
+        std::make_unique<AutofillContextMenuManager>(
+            render_view_context_menu_.get(), menu_model_.get());
+  }
+
   // Sets the `form` and the `form.fields`'s `host_frame`. Since this test
   // fixture has its own render frame host, which is used by the
   // `autofill_context_menu_manager()`, this is necessary to identify the forms
@@ -564,6 +575,10 @@
     // Load an HTML with password forms so that the test can execute JS on the
     // forms.
     ASSERT_TRUE(embedded_test_server()->Start());
+  }
+
+ protected:
+  void LoadPasswordForm() {
     PasswordsNavigationObserver observer(web_contents());
     const GURL url =
         embedded_test_server()->GetURL("/password/password_form.html");
@@ -584,20 +599,96 @@
         ->GetPersonalDataManager()
         .test_address_data_manager()
         .SetAutofillProfileEnabled(false);
-
-    FormData form = CreateAndAttachPasswordForm();
-    autofill_context_menu_manager()->set_params_for_testing(
-        CreateContextMenuParams(form.renderer_id(),
-                                form.fields()[0].renderer_id(),
-                                blink::mojom::FormControlType::kInputPassword));
   }
+
+  void LoadCredentiallessIframe() {
+    PasswordsNavigationObserver observer(web_contents());
+    const GURL url = embedded_test_server()->GetURL(
+        "/password/password_form_in_credentialless_iframe.html");
+    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+    ASSERT_TRUE(observer.Wait());
+
+    // Create a credentialless iframe and attach it to the main frame.
+    ASSERT_TRUE(content::ExecJs(
+        main_rfh(), R"(create_iframe('/empty.html', 'iframe', true);)"));
+    content::RenderFrameHost* child_credentialless_rfh =
+        ChildFrameAt(main_rfh(), 0);
+    ASSERT_NE(child_credentialless_rfh, nullptr);
+
+    CreateAutofillContextMenu(child_credentialless_rfh);
+    autofill_client()
+        ->GetPersonalDataManager()
+        .test_address_data_manager()
+        .SetAutofillProfileEnabled(false);
+  }
+
+  void LoadSandboxedIframe() {
+    PasswordsNavigationObserver observer(web_contents());
+    const GURL url = embedded_test_server()->GetURL(
+        "/password/password_form_in_sandboxed_iframe.html");
+    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+    ASSERT_TRUE(observer.Wait());
+
+    content::RenderFrameHost* child_sandboxed_rfh = ChildFrameAt(main_rfh(), 0);
+    ASSERT_NE(child_sandboxed_rfh, nullptr);
+
+    CreateAutofillContextMenu(child_sandboxed_rfh);
+    autofill_client()
+        ->GetPersonalDataManager()
+        .test_address_data_manager()
+        .SetAutofillProfileEnabled(false);
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_{
+      password_manager::features::kPasswordManualFallbackSecurityChecks};
 };
 
+// Navigates to the `/password/password_form_in_credentialless_iframe.html`
+// page, creates a credentialless iframe and creates the Autofill context menu
+// items for it.
+IN_PROC_BROWSER_TEST_F(PasswordsFallbackWithUIInteractionsTest,
+                       CredentiallessIframe_ManualFallbackNotAdded) {
+  LoadCredentiallessIframe();
+  FormData form = CreateAndAttachUnclassifiedForm();
+  autofill_context_menu_manager()->set_params_for_testing(
+      CreateContextMenuParams(form.renderer_id(),
+                              form.fields()[0].renderer_id(),
+                              blink::mojom::FormControlType::kInputText));
+
+  autofill_context_menu_manager()->AppendItems();
+  EXPECT_THAT(menu_model(), NoAutofillItemsAdded());
+}
+
+// Navigates to the `/password/password_form_in_sandboxed_iframe.html`
+// page and creates the Autofill context menu items for it.
+IN_PROC_BROWSER_TEST_F(PasswordsFallbackWithUIInteractionsTest,
+                       SandboxedIframe_ManualFallbackNotAdded) {
+  LoadSandboxedIframe();
+  FormData form = CreateAndAttachUnclassifiedForm();
+  autofill_context_menu_manager()->set_params_for_testing(
+      CreateContextMenuParams(form.renderer_id(),
+                              form.fields()[0].renderer_id(),
+                              blink::mojom::FormControlType::kInputText));
+
+  autofill_context_menu_manager()->AppendItems();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc b/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc
index 437d939..01fcd5d 100644
--- a/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc
+++ b/chrome/browser/ui/autofill/autofill_context_menu_manager_browsertest.cc
@@ -60,6 +60,7 @@
 #include "components/sync/test/test_sync_service.h"
 #include "components/user_manager/user_names.h"
 #include "content/public/browser/browser_context.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -75,6 +76,12 @@
 using ::testing::ElementsAre;
 using ::testing::IsEmpty;
 using ::testing::Not;
+using ::testing::Pointee;
+using ::testing::Property;
+
+testing::Matcher<const ui::SimpleMenuModel*> NoAutofillItemsAdded() {
+  return Pointee(Property(&ui::SimpleMenuModel::GetItemCount, 0));
+}
 
 // Checks if the context menu model contains any entries with plus address
 // manual fallback labels or command ids. `arg` must be of type
@@ -212,13 +219,7 @@
     ASSERT_TRUE(
         ui_test_utils::NavigateToURL(browser(), GURL("http://test.com")));
 
-    menu_model_ = std::make_unique<ui::SimpleMenuModel>(nullptr);
-    render_view_context_menu_ = std::make_unique<TestRenderViewContextMenu>(
-        *main_rfh(), content::ContextMenuParams());
-    render_view_context_menu_->Init();
-    autofill_context_menu_manager_ =
-        std::make_unique<AutofillContextMenuManager>(
-            render_view_context_menu_.get(), menu_model_.get());
+    CreateAutofillContextMenu(main_rfh());
     autofill_context_menu_manager()->set_params_for_testing(
         CreateContextMenuParams());
   }
@@ -264,6 +265,16 @@
     return autofill_context_menu_manager_.get();
   }
 
+  void CreateAutofillContextMenu(content::RenderFrameHost* rfh) {
+    menu_model_ = std::make_unique<ui::SimpleMenuModel>(nullptr);
+    render_view_context_menu_ = std::make_unique<TestRenderViewContextMenu>(
+        *rfh, content::ContextMenuParams());
+    render_view_context_menu_->Init();
+    autofill_context_menu_manager_ =
+        std::make_unique<AutofillContextMenuManager>(
+            render_view_context_menu_.get(), menu_model_.get());
+  }
+
   // Sets the `form` and the `form.fields`'s `host_frame`. Since this test
   // fixture has its own render frame host, which is used by the
   // `autofill_context_menu_manager()`, this is necessary to identify the forms
@@ -564,6 +575,10 @@
     // Load an HTML with password forms so that the test can execute JS on the
     // forms.
     ASSERT_TRUE(embedded_test_server()->Start());
+  }
+
+ protected:
+  void LoadPasswordForm() {
     PasswordsNavigationObserver observer(web_contents());
     const GURL url =
         embedded_test_server()->GetURL("/password/password_form.html");
@@ -584,20 +599,96 @@
         ->GetPersonalDataManager()
         .test_address_data_manager()
         .SetAutofillProfileEnabled(false);
-
-    FormData form = CreateAndAttachPasswordForm();
-    autofill_context_menu_manager()->set_params_for_testing(
-        CreateContextMenuParams(form.renderer_id(),
-                                form.fields()[0].renderer_id(),
-                                blink::mojom::FormControlType::kInputPassword));
   }
+
+  void LoadCredentiallessIframe() {
+    PasswordsNavigationObserver observer(web_contents());
+    const GURL url = embedded_test_server()->GetURL(
+        "/password/password_form_in_credentialless_iframe.html");
+    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+    ASSERT_TRUE(observer.Wait());
+
+    // Create a credentialless iframe and attach it to the main frame.
+    ASSERT_TRUE(content::ExecJs(
+        main_rfh(), R"(create_iframe('/empty.html', 'iframe', true);)"));
+    content::RenderFrameHost* child_credentialless_rfh =
+        ChildFrameAt(main_rfh(), 0);
+    ASSERT_NE(child_credentialless_rfh, nullptr);
+
+    CreateAutofillContextMenu(child_credentialless_rfh);
+    autofill_client()
+        ->GetPersonalDataManager()
+        .test_address_data_manager()
+        .SetAutofillProfileEnabled(false);
+  }
+
+  void LoadSandboxedIframe() {
+    PasswordsNavigationObserver observer(web_contents());
+    const GURL url = embedded_test_server()->GetURL(
+        "/password/password_form_in_sandboxed_iframe.html");
+    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+    ASSERT_TRUE(observer.Wait());
+
+    content::RenderFrameHost* child_sandboxed_rfh = ChildFrameAt(main_rfh(), 0);
+    ASSERT_NE(child_sandboxed_rfh, nullptr);
+
+    CreateAutofillContextMenu(child_sandboxed_rfh);
+    autofill_client()
+        ->GetPersonalDataManager()
+        .test_address_data_manager()
+        .SetAutofillProfileEnabled(false);
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_{
+      password_manager::features::kPasswordManualFallbackSecurityChecks};
 };
 
+// Navigates to the `/password/password_form_in_credentialless_iframe.html`
+// page, creates a credentialless iframe and creates the Autofill context menu
+// items for it.
+IN_PROC_BROWSER_TEST_F(PasswordsFallbackWithUIInteractionsTest,
+                       CredentiallessIframe_ManualFallbackNotAdded) {
+  LoadCredentiallessIframe();
+  FormData form = CreateAndAttachUnclassifiedForm();
+  autofill_context_menu_manager()->set_params_for_testing(
+      CreateContextMenuParams(form.renderer_id(),
+                              form.fields()[0].renderer_id(),
+                              blink::mojom::FormControlType::kInputText));
+
+  autofill_context_menu_manager()->AppendItems();
+  EXPECT_THAT(menu_model(), NoAutofillItemsAdded());
+}
+
+// Navigates to the `/password/password_form_in_sandboxed_iframe.html`
+// page and creates the Autofill context menu items for it.
+IN_PROC_BROWSER_TEST_F(PasswordsFallbackWithUIInteractionsTest,
+                       SandboxedIframe_ManualFallbackNotAdded) {
+  LoadSandboxedIframe();
+  FormData form = CreateAndAttachUnclassifiedForm();
+  autofill_context_menu_manager()->set_params_for_testing(
+      CreateContextMenuParams(form.renderer_id(),
+                              form.fields()[0].renderer_id(),
+                              blink::mojom::FormControlType::kInputText));
+
+  autofill_context_menu_manager()->AppendItems();
+  EXPECT_THAT(menu_model(), NoAutofillItemsAdded());
+}
+
+// Navigates to the `/password/password_form.html` so that the test can execute
+// JS on forms.
 IN_PROC_BROWSER_TEST_F(
     PasswordsFallbackWithUIInteractionsTest,
     SuggestPasswordTriggersPasswordGenerationAndRecordsMetrics) {
   base::HistogramTester histogram_tester;
 
+  LoadPasswordForm();
+  FormData form = CreateAndAttachPasswordForm();
+  autofill_context_menu_manager()->set_params_for_testing(
+      CreateContextMenuParams(form.renderer_id(),
+                              form.fields()[0].renderer_id(),
+                              blink::mojom::FormControlType::kInputPassword));
+
   // Focus on a password field so that the agent can allow password generation.
   // It is not relevant (and also no in the scope of the test) whether the
   // password field looks the same as the one provided to
diff --git a/chrome/test/data/password/password_form_in_sandboxed_iframe.html b/chrome/test/data/password/password_form_in_sandboxed_iframe.html
new file mode 100644
index 0000000..9d39fbf
--- /dev/null
+++ b/chrome/test/data/password/password_form_in_sandboxed_iframe.html
@@ -0,0 +1,6 @@
+<html>
+<body>
+<iframe sandbox="allow-forms allow-scripts" src="password_form.html" id="iframe" name="iframe">
+</iframe>
+</body>
+</html>
Loading diff…

Original Bug Report

reported by vm...@google.com

Password exfiltration in sandboxed iframe via Autofill manual fallback

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 the Autofill manual fallback flow allows a compromised sandboxed iframe renderer process to access saved passwords of its precursor origin. Because the browser process uses the document URL instead of the opaque origin to query credentials, it fetches exact match credentials and bypasses cross-domain warning gates. Upon user interaction, the browser transmits the plaintext credentials directly to the sandboxed renderer process.

Affected files:

  • components/password_manager/core/browser/password_manual_fallback_flow.cc
  • components/autofill/content/browser/content_autofill_driver.cc
  • components/password_manager/core/browser/password_suggestion_generator.cc
  • components/autofill/content/browser/bad_message.cc

Estimated timestamp from git blame: 2024-04-08

Potential Root Cause Analysis

A potential security boundary bypass exists within the Autofill and Password Manager manual fallback architecture. When a sandboxed iframe (<iframe sandbox="allow-scripts">) commits, it runs within an isolated renderer process with an opaque origin, but retains its precursor origin (e.g., https://victim.com) as its document URL.

1. Precursor-URL-keyed Credentials Query

In components/password_manager/core/browser/password_manual_fallback_flow.cc, the password manual fallback flow derives the credentials search key from the document URL rather than the committed origin:

const GURL origin_as_gurl = password_manager_driver_->GetLastCommittedURL();
password_manager::PasswordFormDigest form_digest(
    password_manager::PasswordForm::Scheme::kHtml,
    password_manager::GetSignonRealm(origin_as_gurl), origin_as_gurl);

For a sandboxed iframe hosted at https://victim.com/sb.html, GetLastCommittedURL() returns the document URL. The signon realm is parsed as https://victim.com/. Consequently, the browser queries the password store for victim.com credentials and treats them as an exact match (match_type = kExact).

2. Cross-Domain Warning Bypass

Because the credentials are categorized as an exact match, PasswordSuggestionGenerator::GetManualFallbackSuggestions (components/password_manager/core/browser/password_suggestion_generator.cc) does not mark the suggestions as cross-domain (is_cross_domain remains false).

Thus, when the user clicks a suggestion, PasswordManualFallbackFlow::EnsureCrossDomainPasswordUsageGetsConsent executes immediately without showing the cross-domain confirmation popup.

3. Delivery of Plaintext Secrets

Once the user selects a suggestion and any biometric checks pass, the browser uses PasswordAutofillAgent::FillPasswordSuggestionById to send the plaintext username and password over Mojo into the sandboxed frame’s renderer process.


Potential Trigger Path

Note: These are potential steps; our tooling currently does not have the capability to execute code to verify a working proof of concept.

  1. A user saves a credential for https://victim.com in their Chrome profile.
  2. The user visits https://victim.com, which embeds <iframe sandbox="allow-scripts" src="https://victim.com/sb.html"> isolated in a separate sandboxed renderer process.
  3. The attacker compromises the sandboxed iframe renderer process.
  4. The compromised renderer issues a mojom::AutofillDriver::AskForValuesToFill IPC with trigger_source set to kManualFallbackPasswords and a PasswordSuggestionRequest targeting a field in the frame.
  5. The browser process bypasses validation because the top-level trigger source is allowed, and dispatches the request to PasswordAutofillManager.
  6. PasswordManualFallbackFlow fetches the victim’s saved credentials for the precursor URL (victim.com) as exact matches.
  7. The user clicks the credentials in the Autofill popup.
  8. The browser process transmits the plaintext credentials directly to the sandboxed renderer over the frame’s PasswordAutofillAgent Mojo interface.

Suggested Remediation

To remediate this issue, the browser process must strictly validate the privilege level of the calling frame before retrieving or filling credentials via the manual fallback flow:

  1. In PasswordAutofillManager::ShowSuggestions, verify whether the driver’s last committed origin is opaque (e.g., password_manager_driver_->GetLastCommittedOrigin().opaque() == true). If it is opaque, restrict or completely disable the manual fallback flow for passwords.
  2. Alternatively, ensure that PasswordManualFallbackFlow uses GetLastCommittedOrigin() rather than GetLastCommittedURL() to define the signon realm and form digest. Opaque origins should trigger the standard cross-domain verification and consent popup, alerting the user to the potential boundary crossing.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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