Overview

Low
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Passwords
DescriptionUI misrepresentation in Passwords
ComponentPasswords
Sub-componentAndroid
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.
Tracker533116484
Fix commit49e2dbf4709a (chromium/src) +63/-16
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/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-28 (public git)
Disclosed2026-09-08
Patch gap42 days — fix public in source before the advisory

Background

Touch To Fill (TTF)
the Android bottom-sheet UI Chrome shows to offer passwords/passkeys for a login request, which displays the origin the credentials are being requested for.
"No Passkeys" bottom sheet
the TTF variant shown for a WebAuthn (navigator.credentials.get) request when no matching passkey exists, presenting the requesting site’s identity to the user.
`GetPrimaryMainFrame()`
a content::WebContents accessor that returns the top-level (outermost) frame of a tab, i.e. the parent document, not any embedded subframe.
`render_frame_host()`
the content::RenderFrameHost of the specific frame that actually initiated the WebAuthn request, which may be a cross-origin subframe.

Root Cause Analysis

The Android WebAuthn TTF delegate computed the origin/URL shown in the “No Passkeys” sheet from the wrong frame. In TouchToFillPasswordManagerWebAuthnDelegate::GetFrameOrigin(), the removed code called credential_receiver_->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(), and GetFrameUrl() called web_contents()->GetLastCommittedURL() — both of which always resolve to the top-level document, ignoring whichever subframe triggered the request. The security invariant that the displayed origin must match the origin that actually initiated the credential request was therefore violated whenever the request came from a subframe.

The fix routes both getters through a new TouchToFillCredentialReceiver::GetFrameUrl()/GetFrameOrigin() interface, implemented by WebAuthnRequestDelegateAndroid as render_frame_host().GetLastCommittedURL()/GetLastCommittedOrigin(), so the identity shown is that of the true requesting frame. The change also adds a credential_receiver_ null guard (previously the raw credential_receiver_->web_contents() deref could crash) and makes the getters const.

Key insight
The single mistake is reading the main frame’s origin (GetPrimaryMainFrame()) instead of the requesting frame’s origin; the fix delegates to the receiver’s render_frame_host(), so a subframe request now displays its own origin rather than the parent’s.

Attack Path

  1. Embed a cross-origin subframe An attacker-controlled page (or a page under a trusted-looking parent) hosts a subframe from a different origin that is permitted to make WebAuthn calls.
  2. Trigger a WebAuthn get from the subframe The subframe calls navigator.credentials.get({publicKey: ...}) for a relying party where the user has no passkey, causing Chrome on Android to prepare the “No Passkeys” bottom sheet.
  3. Origin resolved from the wrong frame GetFrameOrigin()/GetFrameUrl() return the top-level document’s origin via GetPrimaryMainFrame() instead of the subframe’s, so the sheet labels the request with the parent frame’s identity.
  4. User misled about the requester The victim sees a familiar/trusted parent origin on the passkey prompt while the actual request belongs to the different subframe origin, enabling UI misrepresentation about which site is asking for credentials.

Impact Assessment

An attacker achieves UI spoofing: the Android “No Passkeys” WebAuthn bottom sheet in the browser-process UI displays the parent frame’s origin for a request that in fact originated from a subframe, misleading the user about which site is requesting a passkey. Preconditions are an Android Chrome user, a page embedding a cross-origin subframe able to invoke WebAuthn, and a request that surfaces the no-passkeys sheet. The advisory classifies this as spoofing / UI misrepresentation in Passwords; no memory-corruption or credential-theft escalation is demonstrated by the patch (any further phishing leverage is (inferred)).

Changed Functions

FunctionChangeNotes
GURL
chrome/browser/webauthn/touch_to_fill_credential_receiver.h
modified
WebContents
chrome/browser/webauthn/touch_to_fill_credential_receiver.h
modified
TEST_F
chrome/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_credential_delegate.h
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate_unittest.cc
  • chrome/browser/webauthn/android/webauthn_request_delegate_android.h
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_delegate.h
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.h
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_webauthn_delegate.cc
  • chrome/browser/webauthn/android/webauthn_request_delegate_android.cc
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.cc
  • chrome/browser/webauthn/touch_to_fill_credential_receiver.h

Audit Directions

  • Main-frame-vs-requesting-frame confusion
    Audit any security-relevant UI that displays an origin/URL for GetPrimaryMainFrame() or WebContents::GetLastCommittedURL() usage where the value should come from the specific RenderFrameHost that initiated the action, especially in subframe-capable flows (WebAuthn, payments, permissions).
  • Origin display in credential/permission prompts
    Review other TTF and WebAuthn delegate/receiver getters (and CredMan, autofill sheets) to confirm the shown origin is derived from the requesting frame, not the tab or top-level document.
  • Missing null guards on receiver/web_contents chains
    Search for credential_receiver_-> or web_contents()-> dereferences without a preceding null check, since the removed code could crash when the receiver was absent.
From 49e2dbf4709abf6b1de30e5a27bcdac76e872bec Mon Sep 17 00:00:00 2001
From: Friedrich Horschig <[email protected]>
Date: Tue, 28 Jul 2026 06:46:55 -0700
Subject: [PATCH] [Android][WebAuthn] Fix frame origin retrieval in TTF Delegate

Prior to this CL, the origin of the parent frame would always be used
when the "No Passkeys bottom sheet" shows up for a request originating
from a subframe.

This CL fixes the display issue and the const correctness of the getter.

Fixed: 533116484
Change-Id: Ib1ecba0e949754d487637c30e8aae0a430212c02
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8163621
Reviewed-by: Adem Derinel <[email protected]>
Commit-Queue: Friedrich Hauser <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1669428}
---
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.cc
index 1e874b6..7cc957d4 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.cc
@@ -228,12 +228,13 @@
   std::move(action_completed).Run();
 }
 
-GURL TouchToFillPasswordManagerCredentialDelegate::GetFrameUrl() {
+GURL TouchToFillPasswordManagerCredentialDelegate::GetFrameUrl() const {
   CHECK(filler_);
   return filler_->GetFrameUrl();
 }
 
-url::Origin TouchToFillPasswordManagerCredentialDelegate::GetFrameOrigin() {
+url::Origin TouchToFillPasswordManagerCredentialDelegate::GetFrameOrigin()
+    const {
   CHECK(filler_);
   return filler_->GetFrameOrigin();
 }
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.h b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.h
index c820880..245a2af3 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.h
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_credential_delegate.h
@@ -126,8 +126,8 @@
   void OnHybridSignInSelected(base::OnceClosure action_completed) override;
   void OnDismiss(base::OnceClosure action_completed) override;
   void OnCredManDismissed(base::OnceClosure action_completed) override;
-  GURL GetFrameUrl() override;
-  url::Origin GetFrameOrigin() override;
+  GURL GetFrameUrl() const override;
+  url::Origin GetFrameOrigin() const override;
   bool ShouldShowTouchToFill() override;
   bool ShouldTriggerSubmission() override;
   bool ShouldShowHybridOption() override;
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_delegate.h b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_delegate.h
index 7aff6db..11fd5cc 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_delegate.h
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_password_manager_delegate.h
@@ -64,11 +64,11 @@
 
   // Gets the last committed URL for the frame that triggered this sheet to be
   // created.
-  virtual GURL GetFrameUrl() = 0;
+  virtual GURL GetFrameUrl() const = 0;
 
   // Gets the last committed origin for the frame that triggered this sheet to
   // be created.
-  virtual url::Origin GetFrameOrigin() = 0;
+  virtual url::Origin GetFrameOrigin() const = 0;
 
   // Returns whether TTF is eligible for showing for the currently focused field
   // (e. g. it should not be triggered for the new password field).
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 b40b068..ab61e7a 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
@@ -82,16 +82,13 @@
   std::move(action_completed).Run();
 }
 
-GURL TouchToFillPasswordManagerWebAuthnDelegate::GetFrameUrl() {
-  return credential_receiver_->web_contents()->GetLastCommittedURL();
+GURL TouchToFillPasswordManagerWebAuthnDelegate::GetFrameUrl() const {
+  return credential_receiver_ ? credential_receiver_->GetFrameUrl() : GURL();
 }
 
-url::Origin TouchToFillPasswordManagerWebAuthnDelegate::GetFrameOrigin() {
-  return credential_receiver_->web_contents()
-             ? credential_receiver_->web_contents()
-                   ->GetPrimaryMainFrame()
-                   ->GetLastCommittedOrigin()
-             : url::Origin();
+url::Origin TouchToFillPasswordManagerWebAuthnDelegate::GetFrameOrigin() const {
+  return credential_receiver_ ? credential_receiver_->GetFrameOrigin()
+                              : url::Origin();
 }
 
 bool TouchToFillPasswordManagerWebAuthnDelegate::ShouldShowTouchToFill() {
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 56b3cf1..39aada9 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
@@ -68,8 +68,8 @@
   void OnHybridSignInSelected(base::OnceClosure action_completed) override;
   void OnDismiss(base::OnceClosure action_completed) override;
   void OnCredManDismissed(base::OnceClosure action_completed) override;
-  GURL GetFrameUrl() override;
-  url::Origin GetFrameOrigin() override;
+  GURL GetFrameUrl() const override;
+  url::Origin GetFrameOrigin() const override;
   bool ShouldShowTouchToFill() override;
   bool ShouldTriggerSubmission() override;
   bool ShouldShowHybridOption() override;
diff --git a/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc b/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc
index cde7019e..c9ee628b 100644
--- a/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc
+++ b/chrome/browser/webauthn/android/webauthn_request_delegate_android.cc
@@ -251,3 +251,11 @@
 content::WebContents* WebAuthnRequestDelegateAndroid::web_contents() {
   return content::WebContents::FromRenderFrameHost(&render_frame_host());
 }
+
+GURL WebAuthnRequestDelegateAndroid::GetFrameUrl() const {
+  return render_frame_host().GetLastCommittedURL();
+}
+
+url::Origin WebAuthnRequestDelegateAndroid::GetFrameOrigin() const {
+  return render_frame_host().GetLastCommittedOrigin();
+}
diff --git a/chrome/browser/webauthn/android/webauthn_request_delegate_android.h b/chrome/browser/webauthn/android/webauthn_request_delegate_android.h
index 0b067c6..ee9392d 100644
--- a/chrome/browser/webauthn/android/webauthn_request_delegate_android.h
+++ b/chrome/browser/webauthn/android/webauthn_request_delegate_android.h
@@ -80,6 +80,8 @@
   void OnCredentialSelectionDeclined() override;
   void OnHybridSignInSelected() override;
   content::WebContents* web_contents() override;
+  GURL GetFrameUrl() const override;
+  url::Origin GetFrameOrigin() const override;
 
   // Returns a delegate associated with the |frame_host|. It creates one if
   // one does not already exist.
diff --git a/chrome/browser/webauthn/touch_to_fill_credential_receiver.h b/chrome/browser/webauthn/touch_to_fill_credential_receiver.h
index c963c31..4da9f8d1b 100644
--- a/chrome/browser/webauthn/touch_to_fill_credential_receiver.h
+++ b/chrome/browser/webauthn/touch_to_fill_credential_receiver.h
@@ -9,6 +9,9 @@
 #include <vector>
 
 #include "chrome/browser/webauthn/shared_types.h"
+#include "url/origin.h"
+
+class GURL;
 
 namespace content {
 class WebContents;
@@ -38,6 +41,12 @@
   // Return the WebContents associated with the current WebAuthn request.
   virtual content::WebContents* web_contents() = 0;
 
+  // Return the frame URL associated with the current WebAuthn request.
+  virtual GURL GetFrameUrl() const = 0;
+
+  // Return the frame origin associated with the current WebAuthn request.
+  virtual url::Origin GetFrameOrigin() const = 0;
+
  protected:
   ~TouchToFillCredentialReceiver() = default;
 };
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 02d1027..172a8cd 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
@@ -100,6 +100,14 @@
   MOCK_METHOD(void, OnHybridSignInSelected, (), (override));
 
   content::WebContents* web_contents() override { return web_contents_; }
+  GURL GetFrameUrl() const override {
+    return web_contents_ ? web_contents_->GetLastCommittedURL() : GURL();
+  }
+  url::Origin GetFrameOrigin() const override {
+    return web_contents_
+               ? web_contents_->GetPrimaryMainFrame()->GetLastCommittedOrigin()
+               : url::Origin();
+  }
 
  private:
   raw_ptr<content::WebContents> web_contents_;
@@ -373,3 +381,25 @@
                    std::vector<TouchToFillPasswordManagerView::Credential>,
                    bool)>(webauthn::sorting::SortTouchToFillCredentials)));
 }
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest, GetFrameUrlAndOrigin) {
+  auto delegate = MakeTouchToFillPasswordManagerControllerDelegate(
+      /*should_show_hybrid_option=*/false,
+      /*is_immediate=*/true,
+      /*sorting_callback=*/base::NullCallback());
+
+  EXPECT_EQ(delegate->GetFrameUrl(), GURL(kExampleCom));
+  EXPECT_EQ(delegate->GetFrameOrigin(), url::Origin::Create(GURL(kExampleCom)));
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+       GetFrameUrlAndOriginNullReceiver) {
+  TouchToFillPasswordManagerWebAuthnDelegate delegate(
+      /*receiver=*/nullptr,
+      /*sort_credentials_callback=*/base::NullCallback(),
+      /*should_show_hybrid_option=*/false,
+      /*is_immediate=*/true);
+
+  EXPECT_EQ(delegate.GetFrameUrl(), GURL());
+  EXPECT_TRUE(delegate.GetFrameOrigin().opaque());
+}
Loading diff…

Regression Test / PoC

shipped with the fix
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 02d1027..172a8cd 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
@@ -100,6 +100,14 @@
   MOCK_METHOD(void, OnHybridSignInSelected, (), (override));
 
   content::WebContents* web_contents() override { return web_contents_; }
+  GURL GetFrameUrl() const override {
+    return web_contents_ ? web_contents_->GetLastCommittedURL() : GURL();
+  }
+  url::Origin GetFrameOrigin() const override {
+    return web_contents_
+               ? web_contents_->GetPrimaryMainFrame()->GetLastCommittedOrigin()
+               : url::Origin();
+  }
 
  private:
   raw_ptr<content::WebContents> web_contents_;
@@ -373,3 +381,25 @@
                    std::vector<TouchToFillPasswordManagerView::Credential>,
                    bool)>(webauthn::sorting::SortTouchToFillCredentials)));
 }
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest, GetFrameUrlAndOrigin) {
+  auto delegate = MakeTouchToFillPasswordManagerControllerDelegate(
+      /*should_show_hybrid_option=*/false,
+      /*is_immediate=*/true,
+      /*sorting_callback=*/base::NullCallback());
+
+  EXPECT_EQ(delegate->GetFrameUrl(), GURL(kExampleCom));
+  EXPECT_EQ(delegate->GetFrameOrigin(), url::Origin::Create(GURL(kExampleCom)));
+}
+
+TEST_F(TouchToFillPasswordManagerWebAuthnDelegateTest,
+       GetFrameUrlAndOriginNullReceiver) {
+  TouchToFillPasswordManagerWebAuthnDelegate delegate(
+      /*receiver=*/nullptr,
+      /*sort_credentials_callback=*/base::NullCallback(),
+      /*should_show_hybrid_option=*/false,
+      /*is_immediate=*/true);
+
+  EXPECT_EQ(delegate.GetFrameUrl(), GURL());
+  EXPECT_TRUE(delegate.GetFrameOrigin().opaque());
+}
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.