Chrome · UI
CVE-2026-106380
Spoofing in UI
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc |
modified |
Files Changed
chrome/browser/ui/views/media_router/media_router_dialog_controller_views.ccchrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc
Patch
From f1b87a60fcc4fe038331f5612531b88f3d194f04 Mon Sep 17 00:00:00 2001 From: Ahmed Moussa <[email protected]> Date: Fri, 11 Sep 2026 16:02:20 -0700 Subject: [PATCH] Drop fullscreen when showing presentation GMC dialog When a website requests to start a presentation session via the Presentation API, MediaRouterDialogControllerViews displays the Global Media Controls (GMC) device picker dialog without exiting HTML fullscreen. This could allow a malicious webpage in HTML fullscreen to spoof browser chrome and display the genuine, trusted GMC picker a top attacker-controlled pixels. This CL fixes the issue by acquiring WebContents::ForSecurityDropFullscreen() in ShowMediaRouterDialogForPresentation(), dropping any active HTML fullscreen and blocking re-entry while the dialog is visible. Reset() is updated to release the fullscreen blocker and reset base state even when `ui_` is nullptr (as presentation requests do not initialize `ui_`), and OnWidgetDestroying() only resets when all observed dialog widgets have finished destroying. On ChromeOS (where presentation UI is handled by Ash without Views widget observation), fullscreen is dropped once without persisting the blocker to prevent permanent fullscreen lockout. Also adds an interactive UI test verifying that opening the GMC dialog drops tab fullscreen, prohibits entering fullscreen while open, and restores fullscreen capabilities after closure. TAG=agy CONV=52e343b2-3e67-4853-9d9c-48a9f2723f0b Bug: 553168380 Change-Id: I011c259f76aaed6bb435b5212dd2ead04028ab9d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8383376 Reviewed-by: David Pennington <[email protected]> Commit-Queue: Ahmed Moussa <[email protected]> Cr-Commit-Position: refs/heads/main@{#1696692} --- diff --git a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc index e9b01bc1..4adb35b5 100644 --- a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc +++ b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc @@ -50,9 +50,28 @@ bool MediaRouterDialogControllerViews::ShowMediaRouterDialogForPresentation( std::unique_ptr<StartPresentationContext> context) { + // Block tab fullscreen to prevent UI spoofing. The GMC device picker is a + // trusted browser surface that should not coexist with attacker-controlled + // fullscreen content. + // Note that dropping fullscreen may spin the message loop (e.g. on Windows) + // and destroy the WebContents (and consequently this WebContentsUserData). + base::WeakPtr<MediaRouterDialogControllerViews> weak_this = + weak_ptr_factory_.GetWeakPtr(); + auto blocker = + initiator()->ForSecurityDropFullscreen(display::kInvalidDisplayId); + if (!weak_this || !blocker) { + return false; + } #if BUILDFLAG(IS_CHROMEOS) + // On ChromeOS, presentation UI is delegated to Ash's shelf/system tray + // (which is outside the browser window and does not track widget lifetime + // here). Dropping fullscreen once is sufficient to prevent spoofing without + // permanently blocking fullscreen. ShowGlobalMediaControlsDialog(std::move(context)); #else + // On desktop platforms, keep fullscreen blocked until the MediaDialogView + // widget is destroyed. + fullscreen_blocker_ = std::move(*blocker); ShowGlobalMediaControlsDialogAsync(std::move(context)); #endif // BUILDFLAG(IS_CHROMEOS) return true; @@ -74,9 +93,13 @@ // Block tab fullscreen. There is no toolbar to anchor the cast dialog to in // tab fullscreen mode. It is unsafe to show the dialog entirely within the // content area, as this would make it susceptible to spoofing attacks. + // Note that dropping fullscreen may spin the message loop (e.g. on Windows) + // and destroy the WebContents (and consequently this WebContentsUserData). + base::WeakPtr<MediaRouterDialogControllerViews> weak_this = + weak_ptr_factory_.GetWeakPtr(); auto blocker = initiator()->ForSecurityDropFullscreen(display::kInvalidDisplayId); - if (!blocker) { + if (!weak_this || !blocker) { return; } fullscreen_blocker_ = std::move(*blocker); @@ -139,15 +162,18 @@ } void MediaRouterDialogControllerViews::Reset() { - // If |ui_| is null, Reset() has already been called. + // If |ui_| is null, Reset() has already been called for Cast dialogs. if (ui_) { if (GetActionController()) { GetActionController()->OnDialogHidden(); } ui_.reset(); - fullscreen_blocker_.RunAndReset(); MediaRouterDialogController::Reset(); } + + // Fullscreen blocker must be released regardless of whether |ui_| was set, + // as presentation requests hold a blocker while |ui_| is null. + fullscreen_blocker_.RunAndReset(); } void MediaRouterDialogControllerViews::OnWidgetDestroying( @@ -156,8 +182,13 @@ if (ui_) { ui_->LogMediaSinkStatus(); } - Reset(); + // Remove the observation before calling Reset() and only reset if no other + // dialog widgets are currently observed. This prevents a closing dialog from + // prematurely resetting state while a newer dialog is already active. scoped_widget_observations_.RemoveObservation(widget); + if (!scoped_widget_observations_.IsObservingAnySource()) { + Reset(); + } } void MediaRouterDialogControllerViews::SetDialogCreationCallbackForTesting( diff --git a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc index 6aa0364..1fe4e88 100644 --- a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc +++ b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc @@ -205,6 +205,52 @@ ASSERT_EQ(initiator_, browser()->GetTabStripModel()->GetActiveWebContents()); } +IN_PROC_BROWSER_TEST_F(GlobalMediaControlsDialogTest, + FullscreenExitedOnGMCDialogOpen) { + ASSERT_TRUE(ui_test_utils::NavigateToURL( + browser(), embedded_test_server()->GetURL("/simple_page.html"))); + CreateDialogController(); + + FullscreenController* fullscreen_controller = + ExclusiveAccessManager::From(browser())->fullscreen_controller(); + + // Enter tab fullscreen. + ui_test_utils::FullscreenWaiter enter_waiter(browser(), + {.tab_fullscreen = true}); + EXPECT_TRUE(content::ExecJs(initiator_, + "document.documentElement.requestFullscreen()")); + enter_waiter.Wait(); + EXPECT_TRUE(fullscreen_controller->IsTabFullscreen()); + + // Show GMC dialog for presentation. + ShowDialogForPresentation(); + + // Dialog should be showing and tab fullscreen should be exited. + EXPECT_TRUE(MediaDialogView::IsShowing()); + EXPECT_FALSE(fullscreen_controller->IsTabFullscreen()); + + // While the dialog is showing, tab fullscreen cannot be entered. + EXPECT_FALSE(content::ExecJs(initiator_, + "document.documentElement.requestFullscreen()")); + EXPECT_FALSE(fullscreen_controller->IsTabFullscreen()); + + // Close the dialog. + views::Widget* widget = + MediaDialogView::GetDialogViewForTesting()->GetWidget(); + views::test::WidgetDestroyedWaiter waiter(widget); + MediaDialogView::HideDialog(); + waiter.Wait(); + EXPECT_FALSE(MediaDialogView::IsShowing()); + + // Now we should be able to enter tab fullscreen again. + ui_test_utils::FullscreenWaiter reenter_waiter(browser(), + {.tab_fullscreen = true}); + EXPECT_TRUE(content::ExecJs(initiator_, + "document.documentElement.requestFullscreen()")); + reenter_waiter.Wait(); + EXPECT_TRUE(fullscreen_controller->IsTabFullscreen()); +} + #endif } // namespace media_router
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc
index 6aa0364..1fe4e88 100644
--- a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc
+++ b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc
@@ -205,6 +205,52 @@
ASSERT_EQ(initiator_, browser()->GetTabStripModel()->GetActiveWebContents());
}
+IN_PROC_BROWSER_TEST_F(GlobalMediaControlsDialogTest,
+ FullscreenExitedOnGMCDialogOpen) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), embedded_test_server()->GetURL("/simple_page.html")));
+ CreateDialogController();
+
+ FullscreenController* fullscreen_controller =
+ ExclusiveAccessManager::From(browser())->fullscreen_controller();
+
+ // Enter tab fullscreen.
+ ui_test_utils::FullscreenWaiter enter_waiter(browser(),
+ {.tab_fullscreen = true});
+ EXPECT_TRUE(content::ExecJs(initiator_,
+ "document.documentElement.requestFullscreen()"));
+ enter_waiter.Wait();
+ EXPECT_TRUE(fullscreen_controller->IsTabFullscreen());
+
+ // Show GMC dialog for presentation.
+ ShowDialogForPresentation();
+
+ // Dialog should be showing and tab fullscreen should be exited.
+ EXPECT_TRUE(MediaDialogView::IsShowing());
+ EXPECT_FALSE(fullscreen_controller->IsTabFullscreen());
+
+ // While the dialog is showing, tab fullscreen cannot be entered.
+ EXPECT_FALSE(content::ExecJs(initiator_,
+ "document.documentElement.requestFullscreen()"));
+ EXPECT_FALSE(fullscreen_controller->IsTabFullscreen());
+
+ // Close the dialog.
+ views::Widget* widget =
+ MediaDialogView::GetDialogViewForTesting()->GetWidget();
+ views::test::WidgetDestroyedWaiter waiter(widget);
+ MediaDialogView::HideDialog();
+ waiter.Wait();
+ EXPECT_FALSE(MediaDialogView::IsShowing());
+
+ // Now we should be able to enter tab fullscreen again.
+ ui_test_utils::FullscreenWaiter reenter_waiter(browser(),
+ {.tab_fullscreen = true});
+ EXPECT_TRUE(content::ExecJs(initiator_,
+ "document.documentElement.requestFullscreen()"));
+ reenter_waiter.Wait();
+ EXPECT_TRUE(fullscreen_controller->IsTabFullscreen());
+}
+
#endif
} // namespace media_router
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