Low watch CVSS 4.3 chrome Spoofing 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Transactions Platform
DescriptionUI misrepresentation in Transactions Platform
ComponentTransactions Platform
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.
Tracker514038302
Fix commitc3968d605c3d (chromium/src) +38/-45
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-07-13 (public git)
Disclosed2026-08-25
Patch gap43 days — fix public in source before the advisory

Background

`AutofillPopupHideHelper`
a browser-side WebContentsObserver that watches events (frame deletion, navigation, resize, visibility, zoom) and runs hiding_callback_ to close an Autofill suggestion popup when the frame identified by rfh_id_, the frame that triggered it, stops being valid.
`RenderFrameHost::LifecycleState`
the state of a frame host (kActive, kPendingCommit, kPrerendering, kInBackForwardCache, kPendingDeletion). WebContentsObserver::RenderFrameHostStateChanged reports every transition between states.
Back/forward cache (bfcache)
when a page is navigated away from, its whole frame tree (including subframes) can be kept alive in kInBackForwardCache instead of being deleted, so no RenderFrameDeleted fires for those frames.
`SuggestionHidingReason`
an enum recording why a popup was hidden. This patch removes kNavigation, and frame-lifecycle hides now report kRendererEvent.

Root Cause Analysis

Before the patch, AutofillPopupHideHelper closed the popup in only two lifecycle cases. RenderFrameDeleted handled the triggering frame rfh_id_ being destroyed. DidFinishNavigation handled a cross-document navigation whose GetPreviousRenderFrameHostId() equalled rfh_id_. Neither covers the case the commit message names: a navigation in a parent frame moves the descendant frame that triggered the popup into bfcache. That descendant does not navigate itself, so DidFinishNavigation never reports it as the previous frame. It is not deleted either, so RenderFrameDeleted does not fire. The Autofill popup, which may be owned by an ancestor frame’s AutofillExternalDelegate (per the code comment), stays on screen over a page the user has navigated to.

The fix adds RenderFrameHostStateChanged and hides the popup whenever rfh_id_ moves to any non-kActive state (kInBackForwardCache, kPendingDeletion, and so on). It removes DidFinishNavigation and keeps RenderFrameDeleted only for embedded frame-tree shutdown, where state-change notifications are missing (crbug.com/40693086). This tightens when the popup is torn down.

Key insight
The hide logic was keyed on the triggering frame itself navigating or being deleted. It missed a frame that silently stops being active because an ancestor navigated and the page went into bfcache. Watching RenderFrameHostStateChanged and hiding on any transition out of kActive closes that gap.

Attack Path

  1. Popup opened in a subframe A page contains a subframe with a form field. The user focuses it and Chrome shows an Autofill suggestion popup (for example address or payment suggestions), with rfh_id_ set to that subframe.
  2. Parent-frame navigation The page (or the parent frame’s script) starts a cross-document navigation of an ancestor frame while the popup is open, and the old page is stored in bfcache together with the subframe.
  3. No hide signal The subframe moves to kInBackForwardCache without being deleted and without being the navigation’s previous frame, so neither pre-patch RenderFrameDeleted nor DidFinishNavigation runs hiding_callback_.
  4. Stale popup over new content The browser-rendered Autofill popup stays visible on top of the newly committed page, so the user sees trusted browser UI for one context shown in another, which is the advisory’s “UI misrepresentation”. Any user choice made in the stale popup affecting the new page is (inferred).

Impact Assessment

A web page can make Chrome’s trusted Autofill suggestion UI stay on screen after the frame that triggered it has left the active page, so it appears over different, possibly attacker-controlled, content. This could mislead the user about which site the suggestions or a fill action relate to (inferred). It needs ordinary web content, user interaction to open the popup (focusing a field with saved Autofill data), a parent-frame navigation during that window, and bfcache being able to keep the page. The advisory classifies it as “UI misrepresentation in Transactions Platform” and states no escalation or in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
switch
chrome/browser/ui/autofill/autofill_popup_hide_helper.cc
modified
NavigationHandle
chrome/browser/ui/autofill/autofill_popup_hide_helper.h
modified
RenderFrameHost
chrome/browser/ui/autofill/autofill_popup_hide_helper.h
modified
RenderWidgetHost
chrome/browser/ui/autofill/autofill_popup_hide_helper.h
modified

Files Changed

  • chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
  • chrome/browser/ui/autofill/autofill_popup_hide_helper.h
  • chrome/browser/ui/autofill/autofill_popup_hide_helper.cc
  • components/autofill/core/browser/suggestions/suggestion_hiding_reason.h
  • chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
  • chrome/browser/ui/autofill/email_verifier/email_verification_popup_controller.cc
  • tools/metrics/histograms/metadata/autofill/enums.xml

Audit Directions

  • Browser-UI lifetimes tied to navigation events
    Look for other browser-drawn surfaces anchored to a frame (permission prompts, payment and WebAuthn sheets, password and Autofill bubbles, FedCM, file pickers, color or date pickers) that close on DidFinishNavigation or RenderFrameDeleted but not on RenderFrameHostStateChanged. Test them with a parent-frame navigation that puts the anchoring subframe into bfcache, and with prerender activation.
  • Matching on `GetPreviousRenderFrameHostId`
    Code that compares an observed frame ID to NavigationHandle::GetPreviousRenderFrameHostId() only catches navigations of that frame. Ancestor navigations, same-site RFH reuse and fenced-frame or portal embedding can leave the stored frame inactive without matching.
  • Embedded frame-tree shutdown gaps
    Because RenderFrameHostStateChanged is not sent on FrameTree::Shutdown() (crbug.com/40693086), check that observers relying on it for embedded frame trees (guest views, fenced frames) also handle RenderFrameDeleted or WebContentsDestroyed.
From c3968d605c3d42a036dd6fd6cc4193457e7a6464 Mon Sep 17 00:00:00 2001
From: Christoph Schwering <schwering@google.com>
Date: Mon, 13 Jul 2026 06:16:29 -0700
Subject: [PATCH] [Autofill] Hide popup in RenderFrameHostStateChanged()

The CL replaces RenderFrameDeleted() and DidFinishNavigation() with
RenderFrameHostStateChanged()... but keeps RenderFrameDeleted() again
because of a bug in RenderFrameHostStateChanged().

The intention is that RenderFrameHostStateChanged() is also
notified when a navigation in a parent frame makes the descendant
frame go into bfcache.

Bug: 514038302
Change-Id: I3fae9459774c66233a6236e473ab77c46a62225a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8071266
Reviewed-by: Jan Keitel <jkeitel@google.com>
Commit-Queue: Christoph Schwering <schwering@google.com>
Cr-Commit-Position: refs/heads/main@{#1661091}
---
diff --git a/chrome/browser/ui/autofill/autofill_popup_hide_helper.cc b/chrome/browser/ui/autofill/autofill_popup_hide_helper.cc
index 45ca6723..b4600df 100644
--- a/chrome/browser/ui/autofill/autofill_popup_hide_helper.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_hide_helper.cc
@@ -69,23 +69,42 @@
   }
 }
 
-void AutofillPopupHideHelper::RenderFrameDeleted(
-    content::RenderFrameHost* rfh) {
+void AutofillPopupHideHelper::RenderFrameHostStateChanged(
+    content::RenderFrameHost* rfh,
+    content::RenderFrameHost::LifecycleState old_state,
+    content::RenderFrameHost::LifecycleState new_state) {
+  auto should_hide_popup = [](content::RenderFrameHost::LifecycleState state) {
+    switch (state) {
+      case content::RenderFrameHost::LifecycleState::kActive:
+        return false;
+      case content::RenderFrameHost::LifecycleState::kPendingCommit:
+      case content::RenderFrameHost::LifecycleState::kPrerendering:
+      case content::RenderFrameHost::LifecycleState::kInBackForwardCache:
+      case content::RenderFrameHost::LifecycleState::kPendingDeletion:
+        return true;
+    }
+    NOTREACHED();
+  };
+
   // If the popup menu has been triggered from within an iframe and that frame
   // is deleted, hide the popup. This is necessary because the popup may
   // actually be shown by the `AutofillExternalDelegate` of an ancestor frame,
   // which is not notified about `rfh`'s destruction and therefore won't close
   // the popup.
-  if (rfh_id_ == rfh->GetGlobalId()) {
+  if (rfh_id_ == rfh->GetGlobalId() && should_hide_popup(new_state)) {
     hiding_callback_.Run(SuggestionHidingReason::kRendererEvent);
   }
 }
 
-void AutofillPopupHideHelper::DidFinishNavigation(
-    content::NavigationHandle* navigation_handle) {
-  if (rfh_id_ == navigation_handle->GetPreviousRenderFrameHostId() &&
-      !navigation_handle->IsSameDocument()) {
-    hiding_callback_.Run(SuggestionHidingReason::kNavigation);
+void AutofillPopupHideHelper::RenderFrameDeleted(
+    content::RenderFrameHost* rfh) {
+  // RenderFrameHostStateChanged() is not called when on FrameTree::Shutdown():
+  // crbug.com/40693086.
+  // For the primary frame tree, this is caught by WebContentsDestroyed(), but
+  // for embedded frame trees we observe RenderFrameDeleted() to compensate for
+  // the missing RenderFrameHostStateChanged().
+  if (rfh_id_ == rfh->GetGlobalId()) {
+    hiding_callback_.Run(SuggestionHidingReason::kRendererEvent);
   }
 }
 
diff --git a/chrome/browser/ui/autofill/autofill_popup_hide_helper.h b/chrome/browser/ui/autofill/autofill_popup_hide_helper.h
index 34a5d434..80e5198 100644
--- a/chrome/browser/ui/autofill/autofill_popup_hide_helper.h
+++ b/chrome/browser/ui/autofill/autofill_popup_hide_helper.h
@@ -16,7 +16,6 @@
 #endif  // BUILDFLAG(IS_ANDROID)
 
 namespace content {
-class NavigationHandle;
 class RenderFrameHost;
 class RenderWidgetHost;
 enum class Visibility;
@@ -70,9 +69,11 @@
       content::RenderWidgetHost* render_widget_host) override;
   void PrimaryMainFrameWasResized(bool width_changed) override;
   void OnVisibilityChanged(content::Visibility visibility) override;
+  void RenderFrameHostStateChanged(
+      content::RenderFrameHost* render_frame_host,
+      content::RenderFrameHost::LifecycleState old_state,
+      content::RenderFrameHost::LifecycleState new_state) override;
   void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
-  void DidFinishNavigation(
-      content::NavigationHandle* navigation_handle) override;
 
 #if !BUILDFLAG(IS_ANDROID)
   // ZoomObserver:
diff --git a/chrome/browser/ui/autofill/email_verifier/email_verification_popup_controller.cc b/chrome/browser/ui/autofill/email_verifier/email_verification_popup_controller.cc
index ed79c0b4..b380205c 100644
--- a/chrome/browser/ui/autofill/email_verifier/email_verification_popup_controller.cc
+++ b/chrome/browser/ui/autofill/email_verifier/email_verification_popup_controller.cc
@@ -22,9 +22,6 @@
     case SuggestionHidingReason::kEndEditing:
       return EmailVerificationPopupController::EvpPermissionUiStatus::
           kUserAborted;
-    case SuggestionHidingReason::kNavigation:
-      return EmailVerificationPopupController::EvpPermissionUiStatus::
-          kNavigation;
     case SuggestionHidingReason::kTabGone:
       return EmailVerificationPopupController::EvpPermissionUiStatus::kTabGone;
     case SuggestionHidingReason::kWidgetChanged:
diff --git a/components/autofill/core/browser/suggestions/suggestion_hiding_reason.h b/components/autofill/core/browser/suggestions/suggestion_hiding_reason.h
index 1c93163..42d22bb 100644
--- a/components/autofill/core/browser/suggestions/suggestion_hiding_reason.h
+++ b/components/autofill/core/browser/suggestions/suggestion_hiding_reason.h
@@ -25,8 +25,6 @@
   kFocusChanged = 3,
   // Scrolling or zooming into the page displaces the popup.
   kContentAreaMoved = 4,
-  // A navigation on the page or frame level.
-  kNavigation = 5,
   // The popup is or would become empty.
   kNoSuggestions = 6,
   // The renderer explicitly requested closing the popup.
diff --git a/tools/metrics/histograms/metadata/autofill/enums.xml b/tools/metrics/histograms/metadata/autofill/enums.xml
index e882e67..d601f931 100644
--- a/tools/metrics/histograms/metadata/autofill/enums.xml
+++ b/tools/metrics/histograms/metadata/autofill/enums.xml
@@ -4298,7 +4298,7 @@
   <int value="3" label="Focus removed from field. Follows kEndEditing."/>
   <int value="4"
       label="Scrolling or zooming into the page displaces the popup."/>
-  <int value="5" label="A navigation on the page or frame level."/>
+  <int value="5" label="(Deprecated) A navigation on the page or frame level."/>
   <int value="6" label="The popup is or would become empty."/>
   <int value="7" label="The renderer explicitly requested closing the popup."/>
   <int value="8"
diff --git a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
index db7b0ad6..3c1edf4 100644
--- a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
@@ -185,10 +185,10 @@
                                          SuggestionType::kAutocompleteEntry)});
 
   EXPECT_CALL(manager().external_delegate(),
-              OnSuggestionsHidden(SuggestionHidingReason::kNavigation));
+              OnSuggestionsHidden(SuggestionHidingReason::kRendererEvent));
 
   client().suggestion_controller(manager()).Hide(
-      SuggestionHidingReason::kNavigation);
+      SuggestionHidingReason::kRendererEvent);
 }
 
 TEST_F(AutofillKeyboardAccessoryControllerImplTest,
diff --git a/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc b/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
index 3eba931..9d7c1926 100644
--- a/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
@@ -493,15 +493,7 @@
   ShowSuggestions(manager(), {SuggestionType::kAddressEntry});
   test::GenerateTestAutofillPopup(&manager().external_delegate());
   // The navigation generates a PrimaryMainFrameWasResized callback.
-  SuggestionHidingReason reason;
-  // On Android, keyboard accessory is not hidden if the Chrome native widget
-  // changes its size. The keyboard accessory is still hidden because the input
-  // field looses.
-  if constexpr (BUILDFLAG(IS_ANDROID)) {
-    reason = SuggestionHidingReason::kNavigation;
-  } else {
-    reason = SuggestionHidingReason::kWidgetChanged;
-  }
+  SuggestionHidingReason reason = SuggestionHidingReason::kRendererEvent;
   EXPECT_CALL(client().suggestion_controller(manager()), Hide(reason));
   NavigateAndCommitFrame(main_frame(), GURL("https://bar.com/"));
   // Verify and clear before TearDown() closes the popup.
@@ -514,14 +506,8 @@
        HideInSubFrameOnSubFrameNavigation) {
   ShowSuggestions(sub_manager(), {SuggestionType::kAddressEntry});
   test::GenerateTestAutofillPopup(&sub_manager().external_delegate());
-  if (sub_frame()->ShouldChangeRenderFrameHostOnSameSiteNavigation()) {
-    // If the RenderFrameHost changes, a RenderFrameDeleted will fire first.
-    EXPECT_CALL(client().suggestion_controller(sub_manager()),
-                Hide(SuggestionHidingReason::kRendererEvent));
-  } else {
-    EXPECT_CALL(client().suggestion_controller(sub_manager()),
-                Hide(SuggestionHidingReason::kNavigation));
-  }
+  EXPECT_CALL(client().suggestion_controller(sub_manager()),
+              Hide(SuggestionHidingReason::kRendererEvent));
   NavigateAndCommitFrame(sub_frame(), GURL("https://bar.com/"));
   // Verify and clear before TearDown() closes the popup.
   Mock::VerifyAndClearExpectations(
@@ -539,15 +525,7 @@
        HideInSubFrameOnMainFrameNavigation) {
   ShowSuggestions(sub_manager(), {SuggestionType::kAddressEntry});
   test::GenerateTestAutofillPopup(&sub_manager().external_delegate());
-  SuggestionHidingReason reason;
-  // On Android, keyboard accessory is not hidden if the Chrome native widget
-  // changes its size. The keyboard accessory is still hidden because the input
-  // field looses.
-  if constexpr (BUILDFLAG(IS_ANDROID)) {
-    reason = SuggestionHidingReason::kRendererEvent;
-  } else {
-    reason = SuggestionHidingReason::kWidgetChanged;
-  }
+  SuggestionHidingReason reason = SuggestionHidingReason::kRendererEvent;
   EXPECT_CALL(client().suggestion_controller(sub_manager()), Hide(reason));
   NavigateAndCommitFrame(main_frame(), GURL("https://bar.com/"));
 }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
index db7b0ad6..3c1edf4 100644
--- a/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc
@@ -185,10 +185,10 @@
                                          SuggestionType::kAutocompleteEntry)});
 
   EXPECT_CALL(manager().external_delegate(),
-              OnSuggestionsHidden(SuggestionHidingReason::kNavigation));
+              OnSuggestionsHidden(SuggestionHidingReason::kRendererEvent));
 
   client().suggestion_controller(manager()).Hide(
-      SuggestionHidingReason::kNavigation);
+      SuggestionHidingReason::kRendererEvent);
 }
 
 TEST_F(AutofillKeyboardAccessoryControllerImplTest,
diff --git a/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc b/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
index 3eba931..9d7c1926 100644
--- a/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_suggestion_controller_unittest.cc
@@ -493,15 +493,7 @@
   ShowSuggestions(manager(), {SuggestionType::kAddressEntry});
   test::GenerateTestAutofillPopup(&manager().external_delegate());
   // The navigation generates a PrimaryMainFrameWasResized callback.
-  SuggestionHidingReason reason;
-  // On Android, keyboard accessory is not hidden if the Chrome native widget
-  // changes its size. The keyboard accessory is still hidden because the input
-  // field looses.
-  if constexpr (BUILDFLAG(IS_ANDROID)) {
-    reason = SuggestionHidingReason::kNavigation;
-  } else {
-    reason = SuggestionHidingReason::kWidgetChanged;
-  }
+  SuggestionHidingReason reason = SuggestionHidingReason::kRendererEvent;
   EXPECT_CALL(client().suggestion_controller(manager()), Hide(reason));
   NavigateAndCommitFrame(main_frame(), GURL("https://bar.com/"));
   // Verify and clear before TearDown() closes the popup.
@@ -514,14 +506,8 @@
        HideInSubFrameOnSubFrameNavigation) {
   ShowSuggestions(sub_manager(), {SuggestionType::kAddressEntry});
   test::GenerateTestAutofillPopup(&sub_manager().external_delegate());
-  if (sub_frame()->ShouldChangeRenderFrameHostOnSameSiteNavigation()) {
-    // If the RenderFrameHost changes, a RenderFrameDeleted will fire first.
-    EXPECT_CALL(client().suggestion_controller(sub_manager()),
-                Hide(SuggestionHidingReason::kRendererEvent));
-  } else {
-    EXPECT_CALL(client().suggestion_controller(sub_manager()),
-                Hide(SuggestionHidingReason::kNavigation));
-  }
+  EXPECT_CALL(client().suggestion_controller(sub_manager()),
+              Hide(SuggestionHidingReason::kRendererEvent));
   NavigateAndCommitFrame(sub_frame(), GURL("https://bar.com/"));
   // Verify and clear before TearDown() closes the popup.
   Mock::VerifyAndClearExpectations(
@@ -539,15 +525,7 @@
        HideInSubFrameOnMainFrameNavigation) {
   ShowSuggestions(sub_manager(), {SuggestionType::kAddressEntry});
   test::GenerateTestAutofillPopup(&sub_manager().external_delegate());
-  SuggestionHidingReason reason;
-  // On Android, keyboard accessory is not hidden if the Chrome native widget
-  // changes its size. The keyboard accessory is still hidden because the input
-  // field looses.
-  if constexpr (BUILDFLAG(IS_ANDROID)) {
-    reason = SuggestionHidingReason::kRendererEvent;
-  } else {
-    reason = SuggestionHidingReason::kWidgetChanged;
-  }
+  SuggestionHidingReason reason = SuggestionHidingReason::kRendererEvent;
   EXPECT_CALL(client().suggestion_controller(sub_manager()), Hide(reason));
   NavigateAndCommitFrame(main_frame(), GURL("https://bar.com/"));
 }
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.