Medium watch CVSS 5.4 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in FedCM
DescriptionUI misrepresentation in FedCM
ComponentFedCM
Sub-componentFedCM
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.
Tracker514041087
Fix commit20281770dded (chromium/src) +115/-7
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-05-17 (per vendor advisory)
Fix landed2026-08-12 (public git)
Disclosed2026-09-08
Patch gap27 days — fix public in source before the advisory

Background

FedCM
the Federated Credential Management browser API, which shows a browser-controlled account-selection dialog through which the user consents to sharing an identity with a relying party.
`FedCmAccountSelectionView`
the desktop views implementation that owns the FedCM dialog widget (dialog_widget_) and drives account selection.
`InputEventActivationProtector`
a views helper that rejects input events arriving too soon after a UI becomes visible (a ~500ms cooldown), defeating clickjacking / tapjacking where a click lands on a surface that just appeared; it is re-armed by calling VisibilityChanged(true).
`TabDialogManager`
the tab-scoped dialog host that can hide and re-show the FedCM widget (e.g., on tab switch or window resize) without going back through the view’s own show path.

Root Cause Analysis

FedCmAccountSelectionView armed the InputEventActivationProtector by calling input_protector_->VisibilityChanged(true) only on its initial show path (before handing the dialog to TabDialogManager::Params) and reset it with VisibilityChanged(false) on hide. When TabDialogManager re-shows an already-created dialog — for example switching back to the tab, or resizing the window so a bubble dialog that was hidden can fit again — it skips the view’s ShowDialogWidget() path entirely, so VisibilityChanged(true) was never re-invoked and the protector’s cooldown was never restarted. Consequently a click delivered immediately after the dialog re-appeared was accepted by OnAccountSelected instead of being suppressed, so a user could be tricked into confirming an identity choice via an unintended rapid click on the freshly re-shown UI.

The fix makes FedCmAccountSelectionView a views::WidgetObserver, observes dialog_widget_ via dialog_widget_observation_, and routes every widget visibility transition through the new OnWidgetVisibilityChanged, which calls input_protector_->VisibilityChanged(visible) — so the protector is re-armed on every re-show regardless of code path. The unconditional VisibilityChanged(true) on the show path and the VisibilityChanged(false) in the hide path are removed because the observer now covers both.

Key insight
The input-event cooldown was tied to one specific show/hide code path rather than to actual widget visibility, so re-shows driven by TabDialogManager left the protector un-armed; the fix binds re-arming to the widget’s real visibility transitions through views::WidgetObserver::OnWidgetVisibilityChanged.

Attack Path

  1. Show the FedCM dialog A relying party invokes FedCM so the account-selection dialog appears in a tab, managed by TabDialogManager; the initial InputEventActivationProtector cooldown elapses.
  2. Hide then re-show without the view's show path The dialog is hidden and later re-shown by TabDialogManager — e.g., the victim switches away and back to the tab, or a bubble dialog is hidden by a window resize and re-shown when it fits again — which skips ShowDialogWidget() and never re-arms the protector.
  3. Land an immediate click on the re-shown UI Because the protector was not re-armed, an input event arriving right as the dialog re-appears is accepted rather than rejected; an attacker who has baited the user to click at that moment (e.g., timing the re-show under the cursor) has the click routed to OnAccountSelected.
  4. Unintended account confirmation The accepted click confirms an account/identity selection the user did not deliberately make, a UI-misrepresentation / spoofing outcome on the FedCM consent surface.

Impact Assessment

An attacker (a malicious or colluding relying party page) gains the ability to have an unintended, immediate click accepted on the re-shown FedCM account-selection dialog, potentially causing the user to confirm sharing a federated identity without a deliberate interaction. This is a UI-misrepresentation / spoofing issue in the browser UI process, not a memory-safety bug; the precondition is that the FedCM dialog is re-shown via TabDialogManager (tab switch, or resize for bubble dialogs) and that the attacker can induce a click at the moment of re-show. The advisory classifies it as UI misrepresentation in FedCM and does not report in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
modified
FedCmAccountSelectionViewDesktopClickProtectionTest
chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
modified
TEST_F
chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
modified

Files Changed

  • chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
  • chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
  • chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc

Audit Directions

  • Cooldown/anti-clickjacking tied to a single code path
    Audit other uses of InputEventActivationProtector (and similar timing guards) that arm on an explicit show call rather than on actual widget visibility, especially UIs that can be hidden and re-shown by a manager like TabDialogManager.
  • Re-show paths that bypass the view's own show logic
    Look for dialogs/bubbles whose visibility can change via tab activation, window resize, occlusion, or picture-in-picture without re-running their initialization, and verify security-relevant state is re-established on each transition.
  • Consent surfaces requiring deliberate interaction
    Review other security-decision dialogs (permissions, payments, credential sharing) to confirm their input protection re-arms on every re-show, not only first display.
From 20281770dded5b0fbce9474da212dc84b8c41756 Mon Sep 17 00:00:00 2001
From: Yi Gu <yigu@chromium.org>
Date: Wed, 12 Aug 2026 09:17:33 -0700
Subject: [PATCH] [FedCM] Observe dialog widget visibility to update input protector

When the FedCM dialog is managed by TabDialogManager, re-showing it
(e.g., switching back to the tab or resizing the window) skips
ShowDialogWidget() and does not re-arm the InputEventActivationProtector.
This could allow unintended rapid clicks on the re-shown UI.

This CL makes FedCmAccountSelectionView observe the dialog widget's
visibility via views::WidgetObserver, ensuring
InputEventActivationProtector::VisibilityChanged() is invoked on every
visibility transition.

Fixed: 514041087
Change-Id: I1411c8ea360be0bd24cdfe451a5e43a87b3b7a8e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8234102
Reviewed-by: Nicolás Peña <npm@chromium.org>
Commit-Queue: Yi Gu <yigu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1678098}
---
diff --git a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
index 6e0066d3..f287a104 100644
--- a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
+++ b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
@@ -196,7 +196,6 @@
     return;
   }
 
-  input_protector_->VisibilityChanged(true);
   auto params = std::make_unique<tabs::TabDialogManager::Params>();
   params->close_on_navigate = true;
   params->close_on_detach = true;
@@ -657,6 +656,7 @@
                          rp_mode, &dialog_type_);
   }
   dialog_widget_ = CreateDialogWidget();
+  dialog_widget_observation_.Observe(dialog_widget_.get());
   dialog_widget_->MakeCloseSynchronous(base::BindOnce(
       &FedCmAccountSelectionView::OnUserClosedDialog, base::Unretained(this)));
 }
@@ -1259,11 +1259,6 @@
   UpdateDialogVisibility(false);
   scoped_ignore_input_events_.reset();
   tab_accept_mouse_events_.reset();
-  // TODO(crbug.com/331166928): This is only null in one test. Fix the test to
-  // match production.
-  if (input_protector_) {
-    input_protector_->VisibilityChanged(false);
-  }
 }
 
 base::WeakPtr<FedCmAccountSelectionView>
@@ -1277,6 +1272,18 @@
   }
 }
 
+void FedCmAccountSelectionView::OnWidgetVisibilityChanged(views::Widget* widget,
+                                                          bool visible) {
+  if (widget != dialog_widget_.get()) {
+    return;
+  }
+  // TODO(crbug.com/331166928): This is only null in one test. Fix the test to
+  // match production.
+  if (input_protector_) {
+    input_protector_->VisibilityChanged(visible);
+  }
+}
+
 void FedCmAccountSelectionView::ShowMultiAccountPicker(
     const std::vector<IdentityRequestAccountPtr>& accounts,
     const std::vector<IdentityProviderDataPtr>& idp_list,
@@ -1388,6 +1395,7 @@
     parked_dialog_view_.reset();
   }
   scoped_ignore_input_events_.reset();
+  dialog_widget_observation_.Reset();
   dialog_widget_.reset();
   widget_delegate_.reset();
 
@@ -1420,6 +1428,7 @@
   if (should_show_dialog) {
     if (!dialog_widget_) {
       dialog_widget_ = CreateDialogWidget();
+      dialog_widget_observation_.Observe(dialog_widget_.get());
       dialog_widget_->MakeCloseSynchronous(
           base::BindOnce(&FedCmAccountSelectionView::OnUserClosedDialog,
                          base::Unretained(this)));
diff --git a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
index 8f1cdbf..feca0a4e 100644
--- a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
+++ b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
@@ -10,6 +10,7 @@
 #include "base/gtest_prod_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_observer.h"
 #include "chrome/browser/picture_in_picture/scoped_picture_in_picture_occlusion_observation.h"
 #include "chrome/browser/ui/page_action/page_action_observer.h"
@@ -24,6 +25,7 @@
 #include "content/public/browser/web_contents_observer.h"
 #include "ui/views/input_event_activation_protector.h"
 #include "ui/views/widget/widget_delegate.h"
+#include "ui/views/widget/widget_observer.h"
 #include "ui/views/window/dialog_delegate.h"
 
 namespace tabs {
@@ -69,7 +71,8 @@
                                   public FedCmModalDialogView::Observer,
                                   public content::WebContentsObserver,
                                   public PictureInPictureOcclusionObserver,
-                                  public page_actions::PageActionObserver {
+                                  public page_actions::PageActionObserver,
+                                  public views::WidgetObserver {
  public:
   enum class DialogType {
     // FedCM dialog inherits a bubble dialog, which is typically shown on the
@@ -161,6 +164,9 @@
   void CloseModalDialog() override;
   void PrimaryMainFrameWasResized(bool width_changed) override;
 
+  // views::WidgetObserver:
+  void OnWidgetVisibilityChanged(views::Widget* widget, bool visible) override;
+
   base::WeakPtr<FedCmAccountSelectionView> GetWeakPtr();
 
   // Called when the associated tab enters the foreground.
@@ -634,6 +640,9 @@
   // Widget that owns the view.
   std::unique_ptr<views::Widget> dialog_widget_;
 
+  base::ScopedObservation<views::Widget, views::WidgetObserver>
+      dialog_widget_observation_{this};
+
   // This controls the contents of the dialog_widget_. Conceptually there
   // is a view if and only if there is a widget. The two are constructed
   // together and destroyed together. `dialog_widget_` owns
diff --git a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
index e88157f..d7bea92 100644
--- a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
+++ b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
@@ -2695,4 +2695,94 @@
   EXPECT_TRUE(controller->IsDialogWidgetVisible());
 }
 
+class FedCmAccountSelectionViewDesktopClickProtectionTest
+    : public FedCmAccountSelectionViewDesktopTest,
+      public testing::WithParamInterface<blink::mojom::RpMode> {};
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         FedCmAccountSelectionViewDesktopClickProtectionTest,
+                         testing::Values(blink::mojom::RpMode::kPassive,
+                                         blink::mojom::RpMode::kActive));
+
+// Tests that switching to another tab and back re-arms the input event
+// activation protector for both bubble (passive mode) and modal (active mode)
+// dialogs, so that immediate clicks on the re-shown dialog are rejected while
+// delayed clicks after the cooldown period are accepted.
+TEST_P(FedCmAccountSelectionViewDesktopClickProtectionTest,
+       ClickProtectionAfterTabSwitch) {
+  std::unique_ptr<TestFedCmAccountSelectionView> controller =
+      CreateAndShow(accounts_, GetParam());
+
+  // Use a real InputEventActivationProtector.
+  controller->SetInputEventActivationProtectorForTesting(
+      std::make_unique<views::InputEventActivationProtector>());
+
+  // Advance time past the initial 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  // Simulate tab entering background and returning to foreground.
+  TabWillEnterBackground(controller.get());
+  TabForegrounded(controller.get());
+
+  ui::MouseEvent immediate_event(ui::EventType::kMousePressed, gfx::Point(),
+                                 gfx::Point(), base::TimeTicks::Now(),
+                                 ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Immediate click after re-show should be rejected by the protector.
+  EXPECT_FALSE(controller->OnAccountSelected(accounts_[0], immediate_event));
+
+  // Advance past the 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  ui::MouseEvent delayed_event(ui::EventType::kMousePressed, gfx::Point(),
+                               gfx::Point(), base::TimeTicks::Now(),
+                               ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Click after delay should be accepted.
+  EXPECT_TRUE(controller->OnAccountSelected(accounts_[0], delayed_event));
+}
+
+// Tests that when a bubble dialog is hidden because it cannot fit in the web
+// contents during a window resize, re-showing it upon resizing back re-arms the
+// input event activation protector. (Note: Modal dialogs are tab-modal and
+// centered, so they are not hidden when resized).
+TEST_F(FedCmAccountSelectionViewDesktopTest, ClickProtectionAfterResize) {
+  std::unique_ptr<TestFedCmAccountSelectionView> controller =
+      CreateAndShow(accounts_);
+
+  // Use a real InputEventActivationProtector.
+  controller->SetInputEventActivationProtectorForTesting(
+      std::make_unique<views::InputEventActivationProtector>());
+
+  // Advance time past the initial 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  // Resize so bubble dialog cannot fit, which hides it.
+  controller->can_fit_in_web_contents_ = false;
+  controller->PrimaryMainFrameWasResized(/*width_changed=*/true);
+  EXPECT_FALSE(controller->IsDialogWidgetVisible());
+
+  // Resize back so dialog can fit again, which re-shows it.
+  controller->can_fit_in_web_contents_ = true;
+  controller->PrimaryMainFrameWasResized(/*width_changed=*/true);
+  EXPECT_TRUE(controller->IsDialogWidgetVisible());
+
+  ui::MouseEvent immediate_event(ui::EventType::kMousePressed, gfx::Point(),
+                                 gfx::Point(), base::TimeTicks::Now(),
+                                 ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Immediate click after re-show should be rejected.
+  EXPECT_FALSE(controller->OnAccountSelected(accounts_[0], immediate_event));
+
+  // Advance past the 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  ui::MouseEvent delayed_event(ui::EventType::kMousePressed, gfx::Point(),
+                               gfx::Point(), base::TimeTicks::Now(),
+                               ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Click after delay should be accepted.
+  EXPECT_TRUE(controller->OnAccountSelected(accounts_[0], delayed_event));
+}
+
 }  // namespace webid
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
index e88157f..d7bea92 100644
--- a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
+++ b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
@@ -2695,4 +2695,94 @@
   EXPECT_TRUE(controller->IsDialogWidgetVisible());
 }
 
+class FedCmAccountSelectionViewDesktopClickProtectionTest
+    : public FedCmAccountSelectionViewDesktopTest,
+      public testing::WithParamInterface<blink::mojom::RpMode> {};
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         FedCmAccountSelectionViewDesktopClickProtectionTest,
+                         testing::Values(blink::mojom::RpMode::kPassive,
+                                         blink::mojom::RpMode::kActive));
+
+// Tests that switching to another tab and back re-arms the input event
+// activation protector for both bubble (passive mode) and modal (active mode)
+// dialogs, so that immediate clicks on the re-shown dialog are rejected while
+// delayed clicks after the cooldown period are accepted.
+TEST_P(FedCmAccountSelectionViewDesktopClickProtectionTest,
+       ClickProtectionAfterTabSwitch) {
+  std::unique_ptr<TestFedCmAccountSelectionView> controller =
+      CreateAndShow(accounts_, GetParam());
+
+  // Use a real InputEventActivationProtector.
+  controller->SetInputEventActivationProtectorForTesting(
+      std::make_unique<views::InputEventActivationProtector>());
+
+  // Advance time past the initial 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  // Simulate tab entering background and returning to foreground.
+  TabWillEnterBackground(controller.get());
+  TabForegrounded(controller.get());
+
+  ui::MouseEvent immediate_event(ui::EventType::kMousePressed, gfx::Point(),
+                                 gfx::Point(), base::TimeTicks::Now(),
+                                 ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Immediate click after re-show should be rejected by the protector.
+  EXPECT_FALSE(controller->OnAccountSelected(accounts_[0], immediate_event));
+
+  // Advance past the 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  ui::MouseEvent delayed_event(ui::EventType::kMousePressed, gfx::Point(),
+                               gfx::Point(), base::TimeTicks::Now(),
+                               ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Click after delay should be accepted.
+  EXPECT_TRUE(controller->OnAccountSelected(accounts_[0], delayed_event));
+}
+
+// Tests that when a bubble dialog is hidden because it cannot fit in the web
+// contents during a window resize, re-showing it upon resizing back re-arms the
+// input event activation protector. (Note: Modal dialogs are tab-modal and
+// centered, so they are not hidden when resized).
+TEST_F(FedCmAccountSelectionViewDesktopTest, ClickProtectionAfterResize) {
+  std::unique_ptr<TestFedCmAccountSelectionView> controller =
+      CreateAndShow(accounts_);
+
+  // Use a real InputEventActivationProtector.
+  controller->SetInputEventActivationProtectorForTesting(
+      std::make_unique<views::InputEventActivationProtector>());
+
+  // Advance time past the initial 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  // Resize so bubble dialog cannot fit, which hides it.
+  controller->can_fit_in_web_contents_ = false;
+  controller->PrimaryMainFrameWasResized(/*width_changed=*/true);
+  EXPECT_FALSE(controller->IsDialogWidgetVisible());
+
+  // Resize back so dialog can fit again, which re-shows it.
+  controller->can_fit_in_web_contents_ = true;
+  controller->PrimaryMainFrameWasResized(/*width_changed=*/true);
+  EXPECT_TRUE(controller->IsDialogWidgetVisible());
+
+  ui::MouseEvent immediate_event(ui::EventType::kMousePressed, gfx::Point(),
+                                 gfx::Point(), base::TimeTicks::Now(),
+                                 ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Immediate click after re-show should be rejected.
+  EXPECT_FALSE(controller->OnAccountSelected(accounts_[0], immediate_event));
+
+  // Advance past the 500ms protection window.
+  task_environment()->FastForwardBy(base::Milliseconds(600));
+
+  ui::MouseEvent delayed_event(ui::EventType::kMousePressed, gfx::Point(),
+                               gfx::Point(), base::TimeTicks::Now(),
+                               ui::EF_LEFT_MOUSE_BUTTON, 0);
+
+  // Click after delay should be accepted.
+  EXPECT_TRUE(controller->OnAccountSelected(accounts_[0], delayed_event));
+}
+
 }  // namespace webid
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential FedCM clickjacking protection bypass on re-show via TabDialogManager

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 FedCM account selection bubble fails to re-arm its clickjacking protection delay when re-shown after being hidden by the TabDialogManager. This allows an attacker to bypass the mandatory 500ms interaction delay by hiding and then immediately re-showing the bubble via a tab switch. An attacker could exploit this to trick a user into unintended account selection and identity disclosure.

Affected files:

  • chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
  • chrome/browser/ui/tabs/tab_dialog_manager.cc
  • ui/views/input_event_activation_protector.cc

Estimated timestamp from git blame: 2025-08-29

Technical Analysis

The FedCmAccountSelectionView on desktop fails to re-arm the InputEventActivationProtector when the account selection bubble is re-shown after being hidden by the TabDialogManager (e.g., during a tab switch). This creates a vulnerability where the 500ms protective delay against unintended interactions is not enforced upon the bubble’s reappearance.

In FedCmAccountSelectionView::UpdateDialogVisibilityAndPosition() (chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc), the logic for showing the dialog is as follows:

if (!dialog_widget_->IsVisible() && !IsDialogManaged(dialog_widget_.get())) {
  ShowDialogWidget();
}
UpdateDialogVisibility(true);

The ShowDialogWidget() method is responsible for calling input_protector_->VisibilityChanged(true), which updates the protection timestamp to the current time, ensuring a 500ms delay before inputs are accepted.

However, when a tab containing a FedCM bubble is hidden (e.g., when a user opens a new tab) and then re-shown, IsDialogManaged() returns true. This causes UpdateDialogVisibilityAndPosition() to skip the call to ShowDialogWidget(). Instead, it calls UpdateDialogVisibility(true), which makes the widget visible via the TabDialogManager but does not notify the input_protector_ that visibility has changed.

Consequently, the InputEventActivationProtector continues to use the timestamp from the bubble’s original appearance. If more than 500ms have elapsed since that initial show, any interaction with the re-appeared bubble is accepted immediately upon its return to the foreground.

Secondary defenses, such as the WindowsStationarityMonitor, are also bypassed in this scenario. Because the browser window’s bounds remain unchanged and no WindowTreeHost is created or destroyed during a simple tab-switch cycle, the monitor does not detect a lack of stationarity and fails to trigger a timestamp update.

Potential Step-by-Step Reproduction

  1. Initial Request: A user navigates to an attacker-controlled site that triggers a FedCM account selection bubble.
  2. Wait: The attacker site waits for at least 500ms to ensure the initial protection interval has passed.
  3. Tab Switch: The attacker site opens a new foreground tab in the same window (requiring a user gesture). The TabDialogManager hides the FedCM bubble in the now-background tab.
  4. Decoy Interaction: The new tab displays a decoy button at the same screen coordinates as an account selection row in the original FedCM bubble.
  5. Trigger Re-show: The attacker site closes the foreground tab (e.g., via window.close()) just as the user is expected to click the decoy.
  6. Clickjacking: The original tab gains focus and the FedCM bubble is re-shown. The user’s click lands on the re-shown bubble and is processed immediately, bypassing the intended security delay and disclosing the user’s identity to the attacker.

Suggested Fix

Ensure that input_protector_->VisibilityChanged(true) is called whenever the dialog’s visibility is set to true, regardless of whether it is managed by the TabDialogManager. A potential fix would be to move the VisibilityChanged(true) call into UpdateDialogVisibility(true) or ensure it is called explicitly in UpdateDialogVisibilityAndPosition whenever the dialog is about to be re-shown.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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.

View on issue tracker