Overview

Low
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Passwords
DescriptionUI misrepresentation in Passwords
ComponentPasswords
Sub-componentAutofill
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker533044125
Fix commit10c3ba83d367 (chromium/src) +52/-1
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

Background

Keyboard replacing surface
an Android autofill surface (Touch To Fill / TTF or the CredMan credential picker) that replaces the on-screen keyboard to let the user pick a saved password or passkey for a focused field.
`ContinueShowKeyboardReplacingSurface`
the ChromePasswordManagerClient continuation that runs asynchronously after credentials (including passkeys) have been fetched and actually shows the surface and marks its visibility.
`CanShowAutofillUi()`
a driver method that reports whether the driver’s RenderFrameHost is currently in a state allowed to display autofill UI (i.e. an active frame), returning false for inactive frames such as one left behind after a navigation (inferred: the exact active-frame check is implied by the fix and the test’s IsActive() assertion).
`weak_driver`
a base::WeakPtr to the password-manager driver that requested the surface; it survives even when the underlying frame becomes inactive, so its mere non-null validity does not prove the frame is still showable.

Root Cause Analysis

ShowKeyboardReplacingSurface starts an asynchronous flow that requests credentials/passkeys, and only later invokes ContinueShowKeyboardReplacingSurface once results arrive. The pre-patch continuation guarded only against a destroyed driver with if (!weak_driver) { return; }, treating a still-valid weak_driver as sufficient to proceed to show TTF/CredMan and set the surface visible. Between the request and the credentials callback, the user can navigate away, leaving the requesting RenderFrameHost inactive while its weak_driver remains alive; the continuation then displayed the password surface for a frame that no longer owned the visible page, violating the invariant that a keyboard-replacing surface is only shown for the active, foreground frame.

The fix tightens the guard to if (!weak_driver || !weak_driver->CanShowAutofillUi()), so the continuation returns early when the frame is no longer eligible to show autofill UI, suppressing the surface after navigation. Because the guard runs before any InitData/Show call and before visibility is set, no surface is presented for the stale frame, as the regression test’s Times(0) expectations confirm.

Key insight
The single mistake was validating only the liveness of the driver (!weak_driver) rather than its eligibility to show UI, so an asynchronous credentials callback could paint a password surface for a frame the user had already navigated away from; adding !weak_driver->CanShowAutofillUi() to the early-return condition ties the surface to an active frame.

Attack Path

  1. Focus a credential field The victim focuses a password/passkey (webauthn) field on an attacker-influenced page, triggering ShowKeyboardReplacingSurface and an asynchronous request for credentials.
  2. Race the async fetch with a navigation Before the passkey/credential results arrive, the page (or the user) causes a navigation to another origin, leaving the original RenderFrameHost inactive while its weak_driver stays valid.
  3. Credentials arrive for the stale frame The delayed OnCredentialsReceived callback fires and invokes ContinueShowKeyboardReplacingSurface for the now-inactive frame, which passes the old !weak_driver-only guard.
  4. Surface shown over the wrong context Pre-patch, TTF/CredMan is initialized and shown and its visibility is set even though the requesting frame is no longer the active page, misrepresenting which page/origin the credential prompt belongs to (inferred: that this yields a user-deceiving spoof; the patch itself only demonstrates the surface being shown for an inactive frame).

Impact Assessment

An attacker can cause a Touch To Fill / CredMan password-or-passkey surface to be displayed for a frame the user has already navigated away from, producing UI misrepresentation in the Passwords feature on Android — the credential prompt is shown in a context that no longer matches the frame that requested it. The precondition is a focused credential field plus a navigation that races the asynchronous credential fetch, all within the browser process UI on Android. The advisory classifies this as spoofing (UI misrepresentation) and does not report code execution, memory corruption, or in-the-wild exploitation; any escalation to credential theft via user deception is (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/browser/password_manager/chrome_password_manager_client.cc
modified

Files Changed

  • chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
  • chrome/browser/password_manager/chrome_password_manager_client.cc

Audit Directions

  • Async UI continuations validated only for object liveness
    Audit callbacks/continuations that capture a WeakPtr and later show UI; check they re-verify frame/tab activity (e.g. CanShowAutofillUi(), IsActive()), not merely that the weak pointer is non-null.
  • Navigation-race between request and result
    Look for surfaces or dialogs (autofill, permissions, credential pickers) requested on focus/interaction but presented after an async fetch, where a navigation in the interim can leave a stale but valid driver eligible to display.
  • Inactive `RenderFrameHost` reachability
    Search for password/autofill paths that operate on a frame’s driver after prerender activation, BFCache, or cross-document navigation, ensuring each show/visibility sink is gated on the frame still being active.
From 10c3ba83d3674d727b18f64e239bda152bb32e9e Mon Sep 17 00:00:00 2001
From: Friedrich Horschig <[email protected]>
Date: Thu, 30 Jul 2026 07:59:30 -0700
Subject: [PATCH] [Autofill] Check driver CanShowAutofillUi before showing TTF

This CL ensures that
ChromePasswordManagerClient::ContinueShowKeyboardReplacingSurface checks
weak_driver->CanShowAutofillUi() before proceeding to show Touch to Fill
or CredMan UI and setting the keyboard replacing surface visibility.

Fixed: 533044125
Change-Id: If69b90f8fbb48aadaefbdca4800ffd99344452ae
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8174004
Reviewed-by: Ioana Treib <[email protected]>
Commit-Queue: Friedrich Hauser <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1671066}
---
diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 1df9f1f..7fd808b4 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -653,7 +653,7 @@
   // without being called.
   auto split_delay_callback =
       base::SplitOnceCallback(std::move(delay_callback));
-  if (!weak_driver) {
+  if (!weak_driver || !weak_driver->CanShowAutofillUi()) {
     return;
   }
   password_manager::ContentPasswordManagerDriver* driver =
diff --git a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
index 220675f..738ca2fb 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
@@ -92,6 +92,7 @@
 #include "content/public/common/url_constants.h"
 #include "content/public/test/mock_navigation_handle.h"
 #include "content/public/test/navigation_simulator.h"
+#include "content/public/test/prerender_test_util.h"
 #include "content/public/test/web_contents_tester.h"
 #include "mojo/public/cpp/bindings/associated_receiver.h"
 #include "mojo/public/cpp/bindings/pending_associated_receiver.h"
@@ -2590,4 +2591,54 @@
   base::RunLoop().RunUntilIdle();
 }
 
+TEST_F(ChromePasswordManagerClientAndroidTest,
+       DoNotShowKeyboardReplacingSurfaceForInactiveFrame) {
+  webauthn::WebAuthnCredManDelegate::override_cred_man_support_for_testing(
+      webauthn::CredManSupport::DISABLED);
+  CreateManualFillingController(web_contents());
+
+  auto* ttf_controller = MakeMockTouchToFillPasswordManagerController();
+  EXPECT_CALL(*ttf_controller, InitData).Times(0);
+  EXPECT_CALL(*ttf_controller, Show).Times(0);
+
+  constexpr char kUrl[] = "https://www.foo.com/login.html";
+  NavigateAndCommit(GURL(kUrl));
+
+  content::RenderFrameHostWrapper old_rfh(main_rfh());
+  ContentAutofillDriver* autofill_driver =
+      ContentAutofillDriver::GetForRenderFrameHost(old_rfh.get());
+  ASSERT_TRUE(autofill_driver);
+
+  std::vector<FormFieldData> fields = {CreateTestFormField(
+      "Username:", "username", "", FormControlType::kInputText, "webauthn")};
+  FormData form =
+      CreateFormDataForRenderFrameHost(*old_rfh.get(), std::move(fields));
+  {
+    autofill::TestAutofillManagerWaiter waiter(
+        autofill_driver->GetAutofillManager(),
+        {autofill::AutofillManagerEvent::kFormsSeen});
+    autofill_driver->renderer_events().FormsSeen(/*updated_forms=*/{form},
+                                                 /*removed_forms=*/{});
+    ASSERT_TRUE(waiter.Wait(/*num_expected_relevant_events=*/1));
+  }
+
+  auto* old_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(old_rfh.get());
+
+  GetClient()->ShowKeyboardReplacingSurface(
+      old_driver, GetFocusedFieldSuggestionRequest(form));
+
+  // Navigate to another site so old_rfh becomes inactive.
+  NavigateAndCommit(GURL("https://www.bar.com/page2.html"));
+  ASSERT_FALSE(old_rfh.get()->IsActive());
+
+  // Passkeys arrive for the old driver/frame after page navigation.
+  ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents())
+      ->GetDelegateForFrame(old_rfh.get())
+      ->OnCredentialsReceived(
+          std::vector<password_manager::PasskeyCredential>(),
+          ChromeWebAuthnCredentialsDelegate::SecurityKeyOrHybridFlowAvailable(
+              true));
+}
+
 #endif  // BUILDFLAG(IS_ANDROID)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
index 220675f..738ca2fb 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
@@ -92,6 +92,7 @@
 #include "content/public/common/url_constants.h"
 #include "content/public/test/mock_navigation_handle.h"
 #include "content/public/test/navigation_simulator.h"
+#include "content/public/test/prerender_test_util.h"
 #include "content/public/test/web_contents_tester.h"
 #include "mojo/public/cpp/bindings/associated_receiver.h"
 #include "mojo/public/cpp/bindings/pending_associated_receiver.h"
@@ -2590,4 +2591,54 @@
   base::RunLoop().RunUntilIdle();
 }
 
+TEST_F(ChromePasswordManagerClientAndroidTest,
+       DoNotShowKeyboardReplacingSurfaceForInactiveFrame) {
+  webauthn::WebAuthnCredManDelegate::override_cred_man_support_for_testing(
+      webauthn::CredManSupport::DISABLED);
+  CreateManualFillingController(web_contents());
+
+  auto* ttf_controller = MakeMockTouchToFillPasswordManagerController();
+  EXPECT_CALL(*ttf_controller, InitData).Times(0);
+  EXPECT_CALL(*ttf_controller, Show).Times(0);
+
+  constexpr char kUrl[] = "https://www.foo.com/login.html";
+  NavigateAndCommit(GURL(kUrl));
+
+  content::RenderFrameHostWrapper old_rfh(main_rfh());
+  ContentAutofillDriver* autofill_driver =
+      ContentAutofillDriver::GetForRenderFrameHost(old_rfh.get());
+  ASSERT_TRUE(autofill_driver);
+
+  std::vector<FormFieldData> fields = {CreateTestFormField(
+      "Username:", "username", "", FormControlType::kInputText, "webauthn")};
+  FormData form =
+      CreateFormDataForRenderFrameHost(*old_rfh.get(), std::move(fields));
+  {
+    autofill::TestAutofillManagerWaiter waiter(
+        autofill_driver->GetAutofillManager(),
+        {autofill::AutofillManagerEvent::kFormsSeen});
+    autofill_driver->renderer_events().FormsSeen(/*updated_forms=*/{form},
+                                                 /*removed_forms=*/{});
+    ASSERT_TRUE(waiter.Wait(/*num_expected_relevant_events=*/1));
+  }
+
+  auto* old_driver =
+      ContentPasswordManagerDriver::GetForRenderFrameHost(old_rfh.get());
+
+  GetClient()->ShowKeyboardReplacingSurface(
+      old_driver, GetFocusedFieldSuggestionRequest(form));
+
+  // Navigate to another site so old_rfh becomes inactive.
+  NavigateAndCommit(GURL("https://www.bar.com/page2.html"));
+  ASSERT_FALSE(old_rfh.get()->IsActive());
+
+  // Passkeys arrive for the old driver/frame after page navigation.
+  ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents())
+      ->GetDelegateForFrame(old_rfh.get())
+      ->OnCredentialsReceived(
+          std::vector<password_manager::PasskeyCredential>(),
+          ChromeWebAuthnCredentialsDelegate::SecurityKeyOrHybridFlowAvailable(
+              true));
+}
+
 #endif  // BUILDFLAG(IS_ANDROID)
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.