Chrome · Autofill
CVE-2026-14134
Logic Error in Autofill
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/android_autofill/browser/android_autofill_provider.cc |
modified |
Files Changed
components/android_autofill/browser/android_autofill_provider.cccomponents/android_autofill/browser/android_autofill_provider.h
Patch
From b84e41ea4c7e98cb7f5826f63eab9c45d07c294f Mon Sep 17 00:00:00 2001
From: Jihad Hanna <jihadghanna@google.com>
Date: Wed, 20 May 2026 10:16:31 -0700
Subject: [PATCH] Route Passkey events to the CredManDelegate of the correct frame
Fixed: 514055973
Change-Id: I19cb9a1cef2964997a7d5d5868491891bd5128f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7864240
Reviewed-by: Christoph Schwering <schwering@google.com>
Commit-Queue: Jihad Hanna <jihadghanna@google.com>
Reviewed-by: Friedrich Hauser <friedrichh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633670}
---
diff --git a/components/android_autofill/browser/android_autofill_provider.cc b/components/android_autofill/browser/android_autofill_provider.cc
index 4af27bc..5acc410f 100644
--- a/components/android_autofill/browser/android_autofill_provider.cc
+++ b/components/android_autofill/browser/android_autofill_provider.cc
@@ -36,6 +36,7 @@
#include "components/webauthn/android/webauthn_cred_man_delegate_factory.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/android/window_android.h"
#include "ui/gfx/geometry/rect_f.h"
@@ -98,16 +99,25 @@
.render_frame_host();
}
+content::RenderFrameHost* GetRenderFrameHost(
+ AutofillManager* manager,
+ const LocalFrameToken& frame_token) {
+ content::RenderFrameHost* result = nullptr;
+ GetRenderFrameHost(manager)->ForEachRenderFrameHost(
+ [&result, &frame_token](content::RenderFrameHost* rfh) {
+ if (LocalFrameToken(rfh->GetFrameToken().value()) == frame_token) {
+ result = rfh;
+ }
+ });
+ return result;
+}
+
WebAuthnCredManDelegate* GetCredManDelegate(content::RenderFrameHost* rfh) {
return WebAuthnCredManDelegateFactory::GetFactory(
content::WebContents::FromRenderFrameHost(rfh))
->GetRequestDelegate(rfh);
}
-WebAuthnCredManDelegate* GetCredManDelegate(AutofillManager* manager) {
- return GetCredManDelegate(GetRenderFrameHost(manager));
-}
-
bool AllowCredManOnField(const FormFieldData& field) {
return field.parsed_autocomplete() && field.parsed_autocomplete()->webauthn;
}
@@ -204,13 +214,10 @@
session_state_.emplace();
}
- GetRenderFrameHost(manager)->ForEachRenderFrameHost(
- [this, &field](content::RenderFrameHost* rfh) {
- LocalFrameToken frame_token(rfh->GetFrameToken().value());
- if (frame_token == field.host_frame()) {
- session_state_->last_queried_field_rfh_id = rfh->GetGlobalId();
- }
- });
+ if (content::RenderFrameHost* rfh =
+ GetRenderFrameHost(manager, field.host_frame())) {
+ session_state_->last_queried_field_rfh_id = rfh->GetGlobalId();
+ }
UpdateCurrentField(manager, form, field);
@@ -426,22 +433,34 @@
}
bool AndroidAutofillProvider::HasPasskeyRequest() {
- if (!session_state_ || !session_state_->manager || !session_state_->form ||
- !GetCredManDelegate(GetRenderFrameHost(session_state_->manager.get()))) {
+ if (!session_state_ || !session_state_->manager || !session_state_->form) {
return false;
}
+
const FormFieldData* field = session_state_->form->form().FindFieldByGlobalId(
session_state_->current_field.id);
- return field && AllowCredManOnField(*field);
+ if (!field || !AllowCredManOnField(*field)) {
+ return false;
+ }
+
+ content::RenderFrameHost* rfh =
+ GetRenderFrameHost(session_state_->manager.get(), field->host_frame());
+ return rfh && GetCredManDelegate(rfh);
}
void AndroidAutofillProvider::OnTriggerPasskeyRequest() {
- if (session_state_ && session_state_->manager) {
- if (content::RenderFrameHost* rfh =
- GetRenderFrameHost(session_state_->manager.get())) {
- if (WebAuthnCredManDelegate* delegate = GetCredManDelegate(rfh)) {
- delegate->TriggerCredManUi(RequestPasswords(false));
- }
+ if (!session_state_ || !session_state_->manager || !session_state_->form) {
+ return;
+ }
+ const FormFieldData* field = session_state_->form->form().FindFieldByGlobalId(
+ session_state_->current_field.id);
+ if (!field) {
+ return;
+ }
+ if (content::RenderFrameHost* rfh = GetRenderFrameHost(
+ session_state_->manager.get(), field->host_frame())) {
+ if (WebAuthnCredManDelegate* delegate = GetCredManDelegate(rfh)) {
+ delegate->TriggerCredManUi(RequestPasswords(false));
}
}
}
@@ -546,9 +565,10 @@
const FormFieldData& field) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::optional<FieldInfo> field_to_focus = StartFocusChange(form, field);
- if (ShouldShowCredManForField(field, GetRenderFrameHost(manager)) &&
- ShowCredManSheet(GetRenderFrameHost(manager), form.global_id(),
- field_to_focus)) {
+ if (content::RenderFrameHost* rfh =
+ GetRenderFrameHost(manager, field.host_frame());
+ ShouldShowCredManForField(field, rfh) &&
+ ShowCredManSheet(rfh, form.global_id(), field_to_focus)) {
return; // The focus event will be completed after CredMan closes.
}
if (field_to_focus) {
@@ -671,7 +691,9 @@
const FormFieldData* found_field = form_data.FindFieldByGlobalId(field);
const bool intends_to_show_credman =
found_field &&
- IntendsToShowCredMan(*found_field, GetRenderFrameHost(&manager));
+ IntendsToShowCredMan(
+ *found_field,
+ GetRenderFrameHost(&manager, found_field->host_frame()));
return intends_to_show_credman ||
(ArePrefillRequestsSupported() && !has_used_cached_form_ &&
cached_data_ && cached_data_->cached_form &&
@@ -737,6 +759,7 @@
CHECK_EQ(credman_sheet_status_, CredManBottomSheetLifecycle::kNotShown);
if (WebAuthnCredManDelegate* delegate = GetCredManDelegate(rfh)) {
credman_sheet_status_ = CredManBottomSheetLifecycle::kIsShowing;
+ credman_sheet_rfh_id_ = rfh->GetGlobalId();
delegate->SetRequestCompletionCallback(base::BindRepeating(
&AndroidAutofillProvider::OnCredManUiClosed,
weak_ptr_factory_.GetWeakPtr(), std::move(form_id),
@@ -779,11 +802,14 @@
}
void AndroidAutofillProvider::Reset() {
- if (session_state_ && session_state_->manager) {
- if (WebAuthnCredManDelegate* delegate =
- GetCredManDelegate(session_state_->manager.get())) {
- delegate->SetRequestCompletionCallback(base::DoNothing());
+ if (credman_sheet_rfh_id_) {
+ if (content::RenderFrameHost* rfh =
+ content::RenderFrameHost::FromID(credman_sheet_rfh_id_)) {
+ if (WebAuthnCredManDelegate* delegate = GetCredManDelegate(rfh)) {
+ delegate->SetRequestCompletionCallback(base::DoNothing());
+ }
}
+ credman_sheet_rfh_id_ = {};
}
// Clear all session-specific state.
@@ -922,6 +948,7 @@
WebAuthnCredManDelegate::State has_passkeys,
bool success) {
credman_sheet_status_ = CredManBottomSheetLifecycle::kClosed;
+ credman_sheet_rfh_id_ = {};
if (keyboard_suppressor_) {
keyboard_suppressor_->Unsuppress();
}
diff --git a/components/android_autofill/browser/android_autofill_provider.h b/components/android_autofill/browser/android_autofill_provider.h
index bde265d..e7cfabe 100644
--- a/components/android_autofill/browser/android_autofill_provider.h
+++ b/components/android_autofill/browser/android_autofill_provider.h
@@ -14,6 +14,7 @@
#include "components/autofill/core/browser/foundations/autofill_manager.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/webauthn/android/webauthn_cred_man_delegate.h"
+#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/web_contents_observer.h"
namespace content {
@@ -337,6 +338,10 @@
CredManBottomSheetLifecycle credman_sheet_status_ =
CredManBottomSheetLifecycle::kNotShown;
+ // The RenderFrameHost ID of the frame that is currently showing the CredMan
+ // sheet. Used to reset the delegate callback on the correct frame.
+ content::GlobalRenderFrameHostId credman_sheet_rfh_id_;
+
// This is used by the keyboard suppressor. We update it with the result of
// the platform method call `showAutofillDialog`. Since we are not notified
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/android_autofill/browser/android_autofill_provider_unittest.cc b/components/android_autofill/browser/android_autofill_provider_unittest.cc
index b6dea4e..79a3d526 100644
--- a/components/android_autofill/browser/android_autofill_provider_unittest.cc
+++ b/components/android_autofill/browser/android_autofill_provider_unittest.cc
@@ -1060,6 +1060,79 @@
completed_callback.Run(/*success=*/false);
}
+class AndroidAutofillProviderWithCredManMultiFrameTest
+ : public AndroidAutofillProviderWithCredManTest {
+ public:
+ void SetUp() override {
+ AndroidAutofillProviderWithCredManTest::SetUp();
+
+ // Create a subframe, a form in the subframe and register it in the root
+ // manager.
+ sub_frame_ = content::RenderFrameHostTester::For(main_frame())
+ ->AppendChild(std::string("child"));
+ sub_frame_ = NavigateAndCommitFrame(sub_frame_, GURL("https://bar.com"));
+ sub_frame_webauthn_form_ = CreateFormDataForFrame(
+ CreateTestWebAuthnPasswordFormData(),
+ LocalFrameToken(sub_frame_->GetFrameToken().value()));
+ android_autofill_manager().OnFormsSeen({sub_frame_webauthn_form_},
+ /*removed_forms=*/{});
+
+ // Create a mock delegate for the subframe and mock passkeys to be
+ // default-available.
+ auto sub_frame_mock_delegate =
+ std::make_unique<NiceMock<webauthn::MockWebAuthnCredManDelegate>>();
+ sub_frame_mock_delegate_ = sub_frame_mock_delegate.get();
+ webauthn::test_api(web_authn_delegate_factory())
+ .EmplaceDelegateForFrame(sub_frame_,
+ std::move(sub_frame_mock_delegate));
+ ON_CALL(*sub_frame_mock_delegate_, HasPasskeys())
+ .WillByDefault(
+ Return(webauthn::WebAuthnCredManDelegate::State::kHasPasskeys));
+ }
+
+ void TearDown() override {
+ sub_frame_mock_delegate_ = nullptr;
+ sub_frame_ = nullptr;
+ AndroidAutofillProviderWithCredManTest::TearDown();
+ }
+
+ void FocusSubFrameFormField(const FormFieldData& field) {
+ keyboard_suppressor().OnBeforeAskForValuesToFill(
+ android_autofill_manager(), sub_frame_webauthn_form_.global_id(),
+ field.global_id(), sub_frame_webauthn_form_);
+ android_autofill_manager().SimulateOnAskForValuesToFill(
+ sub_frame_webauthn_form_, field);
+ android_autofill_manager().SimulateOnFocusOnFormField(
+ sub_frame_webauthn_form_, field);
+ }
+
+ const FormData& sub_frame_test_form() const {
+ return sub_frame_webauthn_form_;
+ }
+
+ const FormFieldData& sub_frame_webauthn_email_field() const {
+ return sub_frame_test_form().fields()[0];
+ }
+
+ protected:
+ raw_ptr<content::RenderFrameHost> sub_frame_ = nullptr;
+ raw_ptr<webauthn::MockWebAuthnCredManDelegate> sub_frame_mock_delegate_ =
+ nullptr;
+ FormData sub_frame_webauthn_form_;
+};
+
+// Tests that focus events are correctly routed to the delegate of the
+// appropriate frame.
+TEST_F(AndroidAutofillProviderWithCredManMultiFrameTest,
+ CredentialManagerSheetTriggeredOnSubFrameDelegate) {
+ // Expect CredMan UI to be triggered on the subframe's delegate
+ EXPECT_CALL(*sub_frame_mock_delegate_, TriggerCredManUi);
+ // Expect CredMan UI to NOT be triggered on the main frame's delegate
+ EXPECT_CALL(cred_man_delegate(), TriggerCredManUi).Times(0);
+
+ FocusSubFrameFormField(sub_frame_webauthn_email_field());
+}
+
using AndroidAutofillProviderPrefillRequestTest = AndroidAutofillProviderTest;
// Tests that we can send another prefill request after navigation.
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page