Chrome · FedCM
CVE-2026-87462
Logic Error in FedCM
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc |
modified | |
FedCmAccountSelectionViewDesktopClickProtectionTestchrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc |
modified | |
TEST_Fchrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc |
modified |
Files Changed
chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.ccchrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.hchrome/browser/ui/views/webid/fedcm_account_selection_view_desktop_unittest.cc
Patch
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) {
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
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