CVE-2026-113276
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
is_immediate_chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.cc |
modified | |
ifchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.cc |
modified | |
DeviceAuthenticatorchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h |
modified | |
PasskeyCredentialchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h |
modified | |
PasswordManagerClientchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h |
modified | |
UiCredentialchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h |
modified | |
MockPasswordManagerClientchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc |
modified |
Files Changed
chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.ccchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.hchrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.ccchrome/browser/webauthn/android/webauthn_request_delegate_android.cc
Patch
From 0f37f4e8e1f59b8b5e22a275efae58f214214973 Mon Sep 17 00:00:00 2001
From: Ken Buchanan <kenrb@chromium.org>
Date: Tue, 15 Sep 2026 09:27:42 -0700
Subject: [PATCH] [WebAuthn] Password selection in Immediate mode TTF triggers reauth
The Touch To Fill bottom sheet will trigger a biometric reauth for a
password selection when necessary while performing autofill.
The bottom sheet can also be used for WebAuthn Immediate mode requests,
which currently do not trigger a reauth prompt. This change makes
password selection during Immediate mode requests work the same as
during autofill.
Fixed: 517809727
Change-Id: Id1a4067966aaa892f46853f36d36b854b58be811
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8395829
Commit-Queue: Ken Buchanan <kenrb@chromium.org>
Reviewed-by: Friedrich Hauser <friedrichh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1698159}
---
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.cc
index ab61e7a..ffa8f11 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.cc
@@ -8,13 +8,17 @@
#include <utility>
#include <variant>
+#include "base/check.h"
#include "base/containers/span.h"
+#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/notimplemented.h"
#include "chrome/browser/password_manager/android/password_manager_launcher_android.h"
+#include "components/device_reauth/device_authenticator.h"
#include "components/password_manager/core/browser/manage_passwords_referrer.h"
#include "components/password_manager/core/browser/origin_credential_store.h"
#include "components/password_manager/core/browser/passkey_credential.h"
+#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/webauthn/android/webauthn_cred_man_delegate.h"
#include "content/public/browser/web_contents.h"
#include "url/gurl.h"
@@ -25,16 +29,23 @@
TouchToFillPasswordManagerWebAuthnDelegate::
TouchToFillPasswordManagerWebAuthnDelegate(
CredentialReceiver* receiver,
+ password_manager::PasswordManagerClient* password_client,
SortingCallback sort_credentials_callback,
bool should_show_hybrid_option,
bool is_immediate)
: credential_receiver_(receiver),
+ password_client_(password_client),
sort_credentials_callback_(std::move(sort_credentials_callback)),
should_show_hybrid_option_(should_show_hybrid_option),
is_immediate_(is_immediate) {}
TouchToFillPasswordManagerWebAuthnDelegate::
- ~TouchToFillPasswordManagerWebAuthnDelegate() = default;
+ ~TouchToFillPasswordManagerWebAuthnDelegate() {
+ if (authenticator_) {
+ // This is a noop if no auth triggered by Touch To Fill is in progress.
+ authenticator_->Cancel();
+ }
+}
void TouchToFillPasswordManagerWebAuthnDelegate::OnShow(
base::span<const Credential> credentials) {}
@@ -43,9 +54,48 @@
const password_manager::UiCredential& credential,
base::OnceClosure action_complete) {
CHECK(is_immediate_);
- credential_receiver_->OnPasswordCredentialSelected(
- {credential.username(), credential.password()});
- std::move(action_complete).Run();
+ std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator =
+ password_client_ ? password_client_->GetDeviceAuthenticator() : nullptr;
+ if (!password_client_ || !authenticator ||
+ !password_client_->IsReauthBeforeFillingRequired(authenticator.get())) {
+ FillCredential(credential);
+ std::move(action_complete).Run();
+ return;
+ }
+
+ authenticator_ = std::move(authenticator);
+ action_complete_ = std::move(action_complete);
+ // `this` notifies the authenticator when it is destructed, resulting in
+ // the callback being reset by the authenticator. Therefore, it is safe
+ // to use base::Unretained.
+ authenticator_->AuthenticateWithMessage(
+ u"", base::BindOnce(
+ &TouchToFillPasswordManagerWebAuthnDelegate::OnReauthCompleted,
+ base::Unretained(this), credential));
+}
+
+void TouchToFillPasswordManagerWebAuthnDelegate::OnReauthCompleted(
+ password_manager::UiCredential credential,
+ bool auth_successful) {
+ CHECK(action_complete_);
+ if (!auth_successful) {
+ if (credential_receiver_) {
+ credential_receiver_->OnCredentialSelectionDeclined();
+ }
+ std::move(action_complete_).Run();
+ return;
+ }
+
+ FillCredential(credential);
+ std::move(action_complete_).Run();
+}
+
+void TouchToFillPasswordManagerWebAuthnDelegate::FillCredential(
+ const password_manager::UiCredential& credential) {
+ if (credential_receiver_) {
+ credential_receiver_->OnPasswordCredentialSelected(
+ {credential.username(), credential.password()});
+ }
}
void TouchToFillPasswordManagerWebAuthnDelegate::OnPasskeyCredentialSelected(
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h
index c1ade630..9aea5ac 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h
@@ -5,6 +5,7 @@
#ifndef CHROME_BROWSER_TOUCH_TO_FILL_PASSWORD_MANAGER_TOUCH_TO_FILL_PASSWORD_MANAGER_WEBAUTHN_DELEGATE_H_
#define CHROME_BROWSER_TOUCH_TO_FILL_PASSWORD_MANAGER_TOUCH_TO_FILL_PASSWORD_MANAGER_WEBAUTHN_DELEGATE_H_
+#include <memory>
#include <optional>
#include <vector>
@@ -18,8 +19,13 @@
#include "ui/gfx/native_ui_types.h"
#include "url/origin.h"
+namespace device_reauth {
+class DeviceAuthenticator;
+}
+
namespace password_manager {
class PasskeyCredential;
+class PasswordManagerClient;
class UiCredential;
} // namespace password_manager
@@ -40,6 +46,7 @@
TouchToFillPasswordManagerWebAuthnDelegate(
CredentialReceiver* receiver,
+ password_manager::PasswordManagerClient* password_client,
SortingCallback sort_credentials_callback,
bool should_show_hybrid_option,
bool is_immediate);
@@ -76,10 +83,26 @@
gfx::NativeView GetNativeView() override;
private:
+ void OnReauthCompleted(password_manager::UiCredential credential,
+ bool auth_successful);
+
+ void FillCredential(const password_manager::UiCredential& credential);
+
+ // Callback to the controller to be invoked when a finalizing action has
+ // completed. This will result in the destruction of the delegate so
+ // no internal state should be touched after its invocation.
+ base::OnceClosure action_complete_;
+
// Raw pointer to the owning class that will receive the selected credential,
// or other outcomes of the request.
raw_ptr<CredentialReceiver> credential_receiver_ = nullptr;
+ // Weak pointer to the PasswordManagerClient this class is tied to.
+ raw_ptr<password_manager::PasswordManagerClient> password_client_ = nullptr;
+
+ // Authenticator used to trigger a biometric auth before filling.
+ std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator_;
+
SortingCallback sort_credentials_callback_;
bool should_show_hybrid_option_;
diff --git a/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc b/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc
index c9ee628b..e7ee8ee 100644
--- a/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc
+++ b/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc
@@ -14,6 +14,7 @@
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/password_manager/android/grouped_affiliations/acknowledge_grouped_credential_sheet_controller.h"
+#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate_factory.h"
#include "chrome/browser/profiles/profile.h"
@@ -178,10 +179,12 @@
std::move(credentials),
ContentPasswordManagerDriver::GetForRenderFrameHost(frame_host)
->AsWeakPtrImpl());
+ ChromePasswordManagerClient* password_client =
+ ChromePasswordManagerClient::FromWebContents(web_contents());
bool should_show_hybrid_option = !hybrid_closure_.is_null() && !is_immediate;
touch_to_fill_controller_->Show(
std::make_unique<TouchToFillPasswordManagerWebAuthnDelegate>(
- this,
+ this, password_client,
base::BindRepeating<
std::vector<TouchToFillPasswordManagerView::Credential>(
std::vector<TouchToFillPasswordManagerView::Credential>,
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc
index 172a8cd..ac63d21 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc
@@ -9,6 +9,7 @@
#include <string>
#include "base/strings/utf_string_conversions.h"
+#include "base/test/gmock_callback_support.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "chrome/browser/password_manager/android/password_manager_launcher_android.h"
@@ -19,10 +20,12 @@
#include "chrome/browser/webauthn/android/webauthn_request_delegate_android.h"
#include "chrome/browser/webauthn/shared_types.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/device_reauth/mock_device_authenticator.h"
#include "components/password_manager/content/browser/mock_keyboard_replacing_surface_visibility_controller.h"
#include "components/password_manager/core/browser/origin_credential_store.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/webauthn/android/cred_man_support.h"
#include "components/webauthn/android/webauthn_cred_man_delegate.h"
#include "content/public/browser/site_instance.h"
@@ -36,6 +39,8 @@
namespace {
+using base::test::RunOnceCallback;
+using device_reauth::MockDeviceAuthenticator;
using password_manager::PasskeyCredential;
using password_manager::UiCredential;
using Credential = TouchToFillPasswordManagerView::Credential;
@@ -43,8 +48,23 @@
using SortingCallback =
TouchToFillPasswordManagerWebAuthnDelegate::SortingCallback;
using ::testing::_;
+using ::testing::ByMove;
using ::testing::ElementsAreArray;
using ::testing::Eq;
+using ::testing::Return;
+
+class MockPasswordManagerClient
+ : public password_manager::StubPasswordManagerClient {
+ public:
+ MOCK_METHOD(std::unique_ptr<device_reauth::DeviceAuthenticator>,
+ GetDeviceAuthenticator,
+ (),
+ (override));
+ MOCK_METHOD(bool,
+ IsReauthBeforeFillingRequired,
+ (device_reauth::DeviceAuthenticator*),
+ (override));
+};
constexpr char kRpId[] = "example.com";
constexpr char kExampleCom[] = "https://example.com/";
@@ -181,6 +201,7 @@
}
void TearDown() override {
+ authenticator_ = nullptr;
request_delegate_.reset();
web_contents_.reset();
ChromeRenderViewHostTestHarness::TearDown();
@@ -192,6 +213,16 @@
MockJniDelegate& jni_delegate() { return *jni_delegate_; }
+ MockPasswordManagerClient& client() { return client_; }
+
+ MockDeviceAuthenticator* authenticator() { return authenticator_; }
+
+ std::unique_ptr<MockDeviceAuthenticator> CreateMockAuthenticator() {
+ auto authenticator = std::make_unique<MockDeviceAuthenticator>();
+ authenticator_ = authenticator.get();
+ return authenticator;
+ }
+
TouchToFillPasswordManagerController& touch_to_fill_controller() {
return *touch_to_fill_controller_;
}
@@ -204,9 +235,10 @@
MakeTouchToFillPasswordManagerControllerDelegate(
bool should_show_hybrid_option,
bool is_immediate,
- SortingCallback sorting_callback) {
+ SortingCallback sorting_callback,
+ password_manager::PasswordManagerClient* password_client = nullptr) {
return std::make_unique<TouchToFillPasswordManagerWebAuthnDelegate>(
- request_delegate_.get(), std::move(sorting_callback),
+ request_delegate_.get(), password_client, std::move(sorting_callback),
should_show_hybrid_option, is_immediate);
}
@@ -221,6 +253,8 @@
touch_to_fill_controller_;
raw_ptr<MockTouchToFillView> mock_view_ = nullptr;
raw_ptr<MockJniDelegate> jni_delegate_ = nullptr;
+ MockPasswordManagerClient client_;
+ raw_ptr<MockDeviceAuthenticator> authenticator_ = nullptr;
};
TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
@@ -340,6 +374,121 @@
}
TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateWithReauthSuccess) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(CreateMockAuthenticator())));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true));
+ EXPECT_CALL(*authenticator(), AuthenticateWithMessage)
+ .WillOnce(RunOnceCallback<1>(true));
+
+ PasswordCredentialPair expected = {kUserName2, kPassword};
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected(expected));
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateWithReauthFailure) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(CreateMockAuthenticator())));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true));
+ EXPECT_CALL(*authenticator(), AuthenticateWithMessage)
+ .WillOnce(RunOnceCallback<1>(false));
+
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected).Times(0);
+ EXPECT_CALL(request_delegate(), OnCredentialSelectionDeclined());
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateReauthNotRequired) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(CreateMockAuthenticator())));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(false));
+ EXPECT_CALL(*authenticator(), AuthenticateWithMessage).Times(0);
+
+ PasswordCredentialPair expected = {kUserName2, kPassword};
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected(expected));
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateNullAuthenticator) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(nullptr)));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(false));
+
+ PasswordCredentialPair expected = {kUserName2, kPassword};
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected(expected));
... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc
index 172a8cd..ac63d21 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc
@@ -9,6 +9,7 @@
#include <string>
#include "base/strings/utf_string_conversions.h"
+#include "base/test/gmock_callback_support.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "chrome/browser/password_manager/android/password_manager_launcher_android.h"
@@ -19,10 +20,12 @@
#include "chrome/browser/webauthn/android/webauthn_request_delegate_android.h"
#include "chrome/browser/webauthn/shared_types.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/device_reauth/mock_device_authenticator.h"
#include "components/password_manager/content/browser/mock_keyboard_replacing_surface_visibility_controller.h"
#include "components/password_manager/core/browser/origin_credential_store.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/webauthn/android/cred_man_support.h"
#include "components/webauthn/android/webauthn_cred_man_delegate.h"
#include "content/public/browser/site_instance.h"
@@ -36,6 +39,8 @@
namespace {
+using base::test::RunOnceCallback;
+using device_reauth::MockDeviceAuthenticator;
using password_manager::PasskeyCredential;
using password_manager::UiCredential;
using Credential = TouchToFillPasswordManagerView::Credential;
@@ -43,8 +48,23 @@
using SortingCallback =
TouchToFillPasswordManagerWebAuthnDelegate::SortingCallback;
using ::testing::_;
+using ::testing::ByMove;
using ::testing::ElementsAreArray;
using ::testing::Eq;
+using ::testing::Return;
+
+class MockPasswordManagerClient
+ : public password_manager::StubPasswordManagerClient {
+ public:
+ MOCK_METHOD(std::unique_ptr<device_reauth::DeviceAuthenticator>,
+ GetDeviceAuthenticator,
+ (),
+ (override));
+ MOCK_METHOD(bool,
+ IsReauthBeforeFillingRequired,
+ (device_reauth::DeviceAuthenticator*),
+ (override));
+};
constexpr char kRpId[] = "example.com";
constexpr char kExampleCom[] = "https://example.com/";
@@ -181,6 +201,7 @@
}
void TearDown() override {
+ authenticator_ = nullptr;
request_delegate_.reset();
web_contents_.reset();
ChromeRenderViewHostTestHarness::TearDown();
@@ -192,6 +213,16 @@
MockJniDelegate& jni_delegate() { return *jni_delegate_; }
+ MockPasswordManagerClient& client() { return client_; }
+
+ MockDeviceAuthenticator* authenticator() { return authenticator_; }
+
+ std::unique_ptr<MockDeviceAuthenticator> CreateMockAuthenticator() {
+ auto authenticator = std::make_unique<MockDeviceAuthenticator>();
+ authenticator_ = authenticator.get();
+ return authenticator;
+ }
+
TouchToFillPasswordManagerController& touch_to_fill_controller() {
return *touch_to_fill_controller_;
}
@@ -204,9 +235,10 @@
MakeTouchToFillPasswordManagerControllerDelegate(
bool should_show_hybrid_option,
bool is_immediate,
- SortingCallback sorting_callback) {
+ SortingCallback sorting_callback,
+ password_manager::PasswordManagerClient* password_client = nullptr) {
return std::make_unique<TouchToFillPasswordManagerWebAuthnDelegate>(
- request_delegate_.get(), std::move(sorting_callback),
+ request_delegate_.get(), password_client, std::move(sorting_callback),
should_show_hybrid_option, is_immediate);
}
@@ -221,6 +253,8 @@
touch_to_fill_controller_;
raw_ptr<MockTouchToFillView> mock_view_ = nullptr;
raw_ptr<MockJniDelegate> jni_delegate_ = nullptr;
+ MockPasswordManagerClient client_;
+ raw_ptr<MockDeviceAuthenticator> authenticator_ = nullptr;
};
TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
@@ -340,6 +374,121 @@
}
TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateWithReauthSuccess) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(CreateMockAuthenticator())));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true));
+ EXPECT_CALL(*authenticator(), AuthenticateWithMessage)
+ .WillOnce(RunOnceCallback<1>(true));
+
+ PasswordCredentialPair expected = {kUserName2, kPassword};
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected(expected));
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateWithReauthFailure) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(CreateMockAuthenticator())));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true));
+ EXPECT_CALL(*authenticator(), AuthenticateWithMessage)
+ .WillOnce(RunOnceCallback<1>(false));
+
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected).Times(0);
+ EXPECT_CALL(request_delegate(), OnCredentialSelectionDeclined());
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateReauthNotRequired) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(CreateMockAuthenticator())));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(false));
+ EXPECT_CALL(*authenticator(), AuthenticateWithMessage).Times(0);
+
+ PasswordCredentialPair expected = {kUserName2, kPassword};
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected(expected));
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ShowPasswordForImmediateNullAuthenticator) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
+ ElementsAreArray(credentials),
+ TouchToFillPasswordManagerView::kNone));
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(nullptr)));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(false));
+
+ PasswordCredentialPair expected = {kUserName2, kPassword};
+ EXPECT_CALL(request_delegate(), OnPasswordCredentialSelected(expected));
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+ ReauthCancelledOnDestruction) {
+ auto password_credential = CreatePasswordCredential();
+ std::vector<Credential> credentials({CreatePasskey(), password_credential});
+
+ Show(credentials, MakeTouchToFillPasswordManagerControllerDelegate(
+ /*should_show_hybrid_option=*/false,
+ /*is_immediate=*/true, SortingCallback(), &client()));
+
+ EXPECT_CALL(client(), GetDeviceAuthenticator)
+ .WillOnce(Return(ByMove(CreateMockAuthenticator())));
+ ON_CALL(client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true));
+ base::OnceCallback<void(bool)> auth_callback;
+ EXPECT_CALL(*authenticator(), AuthenticateWithMessage)
+ .WillOnce([&auth_callback](const std::u16string& message,
+ base::OnceCallback<void(bool)> callback) {
+ auth_callback = std::move(callback);
+ });
+ EXPECT_CALL(*authenticator(), Cancel).WillOnce([&auth_callback]() {
+ std::move(auth_callback).Run(false);
+ });
+
+ touch_to_fill_controller().OnCredentialSelected(password_credential);
+ touch_to_fill_controller().Close();
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
SortCredentialsForImmediate) {
base::Time time_now = base::Time::Now();
base::Time time_older = time_now - base::Minutes(1);
@@ -396,6 +545,7 @@
GetFrameUrlAndOriginNullReceiver) {
TouchToFillPasswordManagerWebAuthnDelegate delegate(
/*receiver=*/nullptr,
+ /*password_client=*/nullptr,
/*sort_credentials_callback=*/base::NullCallback(),
/*should_show_hybrid_option=*/false,
/*is_immediate=*/true);
Original Bug Report
Biometric reauth gate bypass in Android WebAuthn Touch-To-Fill
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: The WebAuthn Touch-To-Fill implementation on Android fails to check whether biometric or device lock re-authentication is required before filling password credentials. Consequently, selecting a password credential from the WebAuthn bottom sheet returns the cleartext password directly to the renderer without user authentication. This bypasses the security guarantees of the re-authentication gate, which is mandatory on Android Automotive and configurable on standard Android.
Affected files:
chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_webauthn_delegate.ccchrome/browser/webauthn/android/webauthn_request_delegate_android.cc
Estimated timestamp from git blame: 2025-08-01
Description of the Issue
In chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_webauthn_delegate.cc, when a password credential is selected from the WebAuthn Touch-To-Fill (TTF) bottom sheet, the delegate forwards the credential immediately to the credential receiver without checking if device re-authentication is required:
void TouchToFillControllerWebAuthnDelegate::OnCredentialSelected(
const password_manager::UiCredential& credential,
base::OnceClosure action_complete) {
CHECK(is_immediate_);
credential_receiver_->OnPasswordCredentialSelected(
{credential.username(), credential.password()});
std::move(action_complete).Run();
}
The receiver WebAuthnRequestDelegateAndroid also lacks a gate in OnPasswordCredentialSelected:
void WebAuthnRequestDelegateAndroid::OnPasswordCredentialSelected(
const PasswordCredentialPair& password_credential) {
if (password_fetcher_) {
password_fetcher_->UpdateDateLastUsed(password_credential.first,
password_credential.second);
}
if (password_callback_) {
password_callback_.Run(password_credential.first,
password_credential.second);
}
}
The callback then triggers JNI transitions (Fido2CredentialRequest.java), formatting the cleartext credentials into a successful password response, which is sent back over Mojo via the blink.mojom.Authenticator interface to resolve the navigator.credentials.get promise in the renderer process. No biometric, PIN, or pattern verification is prompted to the user on this code path.
Comparison with Secure Siblings
- Standard Autofill Touch-To-Fill (
TouchToFillControllerAutofillDelegate): Correctly checksIsReauthBeforeFillingRequiredand invokesDeviceAuthenticator:if (!password_client_->IsReauthBeforeFillingRequired(authenticator_.get())) { FillCredential(credential); return; } authenticator_->AuthenticateWithMessage( u"", base::BindOnce(&TouchToFillControllerAutofillDelegate::OnReauthCompleted, base::Unretained(this), credential)); - Gate Semantics on Android Automotive:
ChromePasswordManagerClient::IsReauthBeforeFillingRequiredexplicitly mandates re-authentication before filling passwords on Android Automotive. Due to the missing check in the WebAuthn delegate, this security requirement is completely bypassed.
Potential Steps to Reproduce
Note: These are suggested and potential steps, as our tooling agent does not have the capability to execute code or run runtime verification.
- Use a Chrome build on Android Automotive, or on a standard Android phone with
chrome://flags#biometric-reauth-for-password-fillingenabled and “Use screen lock before filling” turned on in Settings. - Ensure a password credential is saved for
https://victim.example. - Navigate to a page on
https://victim.examplecontaining the following script:const cred = await navigator.credentials.get({ mediation: 'immediate', password: true, publicKey: { challenge: new Uint8Array(32), rpId: 'victim.example' } }); console.log("Cleartext password obtained:", cred.password); - Trigger the script. The WebAuthn Touch-to-Fill bottom sheet is presented.
- Select the credential row.
- Observed Result: The Promise resolves, exposing the cleartext password directly to the website without any biometric or device lock prompt.
- Expected Result: A biometric or lock-screen authentication prompt should be presented to the user before the credential is returned, matching standard Autofill TTF behavior.
Suggested Fix
Integrate the DeviceAuthenticator and the PasswordManagerClient::IsReauthBeforeFillingRequired check in TouchToFillControllerWebAuthnDelegate::OnCredentialSelected or WebAuthnRequestDelegateAndroid::OnPasswordCredentialSelected to prevent credentials from being returned to the caller until the user successfully completes authentication.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.