CVE-2026-79212
Overview
Background
- Password manual fallback
- the “Select password” flow that lets a user manually pull up password suggestions for a field, driven by
PasswordAutofillManager::ShowSuggestionswhenfield.trigger_sourceis a manual-fallback source (autofill::IsPasswordsAutofillManuallyTriggered). - `HasValidURL(bool may_kill_renderer)`
- a
PasswordManagerDrivercheck that verifies the current frame’s URL is safe to share password data with, and kills the renderer process when the URL is unsafe andmay_kill_rendereristrue. - `IsRenderFrameHostSupported()`
- a
PasswordManagerDrivercheck that runs additional per-frame security checks to confirm the frame is one password data may be exposed to. - `kPasswordManualFallbackSecurityChecks`
- a feature flag gating the new checks, changed in this CL from desktop-only compilation to being declared/defined on all platforms.
Root Cause Analysis
The password manual fallback entry point in PasswordAutofillManager::ShowSuggestions created and ran a PasswordManualFallbackFlow for manually-triggered password suggestions without re-validating that the triggering frame was actually authorized to receive password data. A prior CL (crrev.com/c/7894835) had added the frame security checks (HasValidURL() and IsRenderFrameHostSupported()) only to the context-menu entry that adds the “Select password” item, so a compromised renderer could skip that entry point and still reach the fallback flow directly, causing the browser to surface/fill saved credentials for an unsafe frame. This CL closes that gap by, when kPasswordManualFallbackSecurityChecks is enabled, computing manual_fallback_allowed_for_frame = HasValidURL() && IsRenderFrameHostSupported() and returning early (showing no suggestions) if it is false, before any PasswordManualFallbackFlow is constructed. To make the check reachable from this browser-side manager, HasValidURL and IsRenderFrameHostSupported are promoted to pure-virtual methods on the PasswordManagerDriver interface and implemented/overridden in the content, stub, and iOS drivers.
The fix works because the same authorization gate that guards the menu entry now also guards the flow itself, so both paths enforce it.
HasValidURL() + IsRenderFrameHostSupported() gate into ShowSuggestions so a compromised frame cannot bypass the menu and trigger fallback suggestions directly.Attack Path
- Compromise or drive a renderer frame An attacker with a compromised renderer (or a frame in an otherwise-unsafe context) crafts a manual-fallback password suggestion request.
- Send a manual-fallback trigger
The frame issues a suggestion request whose
field.trigger_sourceis a manual-fallback source (kManualFallbackPasswords), reachingPasswordAutofillManager::ShowSuggestions. - Bypass the menu-only check
Because the earlier fix only validated the frame when adding the context-menu entry, the pre-patch
ShowSuggestionsproceeds straight to constructingPasswordManualFallbackFlowwithout callingHasValidURL()/IsRenderFrameHostSupported(). - Surface credentials to an unsafe frame The manual fallback flow runs for a frame that failed the security criteria, exposing saved password suggestions to a context that should not receive them (missing authorization).
Impact Assessment
kPasswordManualFallbackSecurityChecks feature being enabled to activate the new gate. No exploited-in-the-wild status is stated in the provided material.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/password_manager/core/browser/password_autofill_manager.cc |
modified | |
TEST_Fcomponents/password_manager/core/browser/password_autofill_manager_unittest.cc |
modified |
Files Changed
components/password_manager/core/browser/features/password_features.hcomponents/password_manager/core/browser/features/password_features.cccomponents/password_manager/core/browser/password_autofill_manager_unittest.cccomponents/password_manager/core/browser/password_autofill_manager.cccomponents/password_manager/content/browser/content_password_manager_driver.hcomponents/password_manager/core/browser/stub_password_manager_driver.hcomponents/password_manager/ios/ios_password_manager_driver.hcomponents/password_manager/ios/ios_password_manager_driver.mmcomponents/password_manager/core/browser/stub_password_manager_driver.cccomponents/password_manager/core/browser/password_manager_driver.h
Audit Directions
- Duplicate entry points sharing one resourceWhen a security check is added to one UI entry point (e.g. a context-menu insertion), audit every other code path that can reach the same sensitive flow (here
PasswordManualFallbackFlow) and confirm each independently enforces the check. - Renderer-influenced trigger sourcesReview consumers of
field.trigger_source/IsPasswordsAutofillManuallyTriggeredand other renderer-supplied signals that gate exposure of saved credentials, ensuring browser-side frame validation precedes any credential surfacing. - Interface method visibilityLook for security checks implemented only as private helpers on a concrete driver; if a sibling manager needs them, they may be missing from the shared
PasswordManagerDriverinterface and thus unenforced on some paths.
Patch
From a08dd63d11f70a9f337b467d1b2dbc5657c50c58 Mon Sep 17 00:00:00 2001
From: Timofey Chudakov <tchudakov@google.com>
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 <ioanap@chromium.org>
Commit-Queue: Timofey Chudakov <tchudakov@google.com>
Reviewed-by: Christoph Schwering <schwering@google.com>
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_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`.
+ virtual bool HasValidURL(bool may_kill_renderer = true) = 0;
+
+ // Performs a number of security checks for the current frame.
+ virtual bool IsRenderFrameHostSupported() = 0;
+
virtual autofill::AutofillDriver* GetAutofillDriver() const = 0;
// Get a WeakPtr to the instance.
diff --git a/components/password_manager/core/browser/stub_password_manager_driver.cc b/components/password_manager/core/browser/stub_password_manager_driver.cc
index a16991ad..eda86f1 100644
--- a/components/password_manager/core/browser/stub_password_manager_driver.cc
+++ b/components/password_manager/core/browser/stub_password_manager_driver.cc
@@ -130,6 +130,14 @@
autofill::FieldRendererId field_id,
base::OnceCallback<void(bool)>) {}
+bool StubPasswordManagerDriver::HasValidURL(bool may_kill_renderer) {
+ return true;
+}
+
+bool StubPasswordManagerDriver::IsRenderFrameHostSupported() {
+ return true;
+}
+
autofill::AutofillDriver* StubPasswordManagerDriver::GetAutofillDriver() const {
return nullptr;
}
diff --git a/components/password_manager/core/browser/stub_password_manager_driver.h b/components/password_manager/core/browser/stub_password_manager_driver.h
index 7d954579..d05bc3f 100644
--- a/components/password_manager/core/browser/stub_password_manager_driver.h
+++ b/components/password_manager/core/browser/stub_password_manager_driver.h
@@ -76,6 +76,8 @@
const gfx::RectF& bounds_in_frame_coordinates) 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/ios/ios_password_manager_driver.h b/components/password_manager/ios/ios_password_manager_driver.h
index 84d163ab..b18f3c0 100644
--- a/components/password_manager/ios/ios_password_manager_driver.h
+++ b/components/password_manager/ios/ios_password_manager_driver.h
@@ -88,6 +88,8 @@
const gfx::RectF& bounds_in_frame_coordinates) 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<PasswordManagerDriver> AsWeakPtr() override;
const std::string& web_frame_id() const { return frame_id_; }
diff --git a/components/password_manager/ios/ios_password_manager_driver.mm b/components/password_manager/ios/ios_password_manager_driver.mm
index a544874..6f0bef8 100644
--- a/components/password_manager/ios/ios_password_manager_driver.mm
+++ b/components/password_manager/ios/ios_password_manager_driver.mm
@@ -222,6 +222,14 @@
NOTREACHED();
}
+bool IOSPasswordManagerDriver::HasValidURL(bool may_kill_renderer) {
+ NOTREACHED();
+}
+
+bool IOSPasswordManagerDriver::IsRenderFrameHostSupported() {
+ NOTREACHED();
+}
+
autofill::AutofillDriver* IOSPasswordManagerDriver::GetAutofillDriver() const {
NOTIMPLEMENTED();
return nullptr;
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;
Regression Test / PoC
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;
Original Bug Report
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.cccomponents/password_manager/core/browser/password_manual_fallback_flow.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.cccomponents/autofill/content/browser/content_autofill_driver.ccchrome/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:
ContentAutofillDriver::AskForValuesToFill(components/autofill/content/browser/content_autofill_driver.cc) accepts the call and runsbad_message::CheckArgswhich permitskManualFallbackPasswordsand only blocklists unrelated trigger sources.- The request is routed via
RouteToManagertoBrowserAutofillManager::OnAskForValuesToFillImpl(components/autofill/core/browser/foundations/browser_autofill_manager.cc). - 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). ShowSuggestionsidentifies the manual fallback trigger source and instantiatesPasswordManualFallbackFlow(components/password_manager/core/browser/password_manual_fallback_flow.cc).- The
PasswordManualFallbackFlowconstructor queriespassword_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/). FormFetcherImplfetches the credentials matchinghttps://victim.example/from the user’s password database.- When generating suggestions, because the matched credential’s realm matches the frame’s precursor URL,
is_cross_domainevaluates tofalseincomponents/password_manager/core/browser/password_suggestion_generator.cc. - 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. - When the user clicks on the acceptable child suggestion,
EnsureCrossDomainPasswordUsageGetsConsentskips the confirmation dialog (sinceis_cross_domainis false) and callsPasswordManagerDriver::FillFieldwith the plaintext password payload. ContentPasswordManagerDriver::FillField(components/password_manager/content/browser/content_password_manager_driver.cc) lacks anyHasValidURL()check and directly forwards the plaintext credential over the Mojo associated interface back to the compromised renderer’sPasswordAutofillAgent.
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:
- Ensure
kIsolateSandboxedIframesis enabled. A victim user has a saved credential forhttps://victim.example/in their Chrome profile. - 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. - The attacker compromises this sandboxed renderer process.
- The compromised renderer directly crafts and sends a
mojom::AutofillDriver::AskForValuesToFillIPC message withtrigger_sourceandpassword_request.field.trigger_sourceset toAutofillSuggestionTriggerSource::kManualFallbackPasswords. - The browser process fetches the credentials matching
https://victim.example/and presents the trusted manual fallback popup to the user. - The user selects the child password-filling suggestion from the popup.
- 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.