CVE-2026-79212
Overview
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/content/browser/content_password_manager_driver.hcomponents/password_manager/core/browser/features/password_features.cccomponents/password_manager/core/browser/features/password_features.hcomponents/password_manager/core/browser/password_autofill_manager.cccomponents/password_manager/core/browser/password_autofill_manager_unittest.cccomponents/password_manager/core/browser/password_manager_driver.h
Patch
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`.
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.