Low chrome Spoofing 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in UI
DescriptionUI misrepresentation in UI
ComponentUI
Bug ClassSpoofing
Tracker553168380
Fix commitf1b87a60fcc4 (chromium/src) +81/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
chrome/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.cc
  • chrome/browser/ui/views/media_router/media_router_dialog_controller_views_interactive_uitest.cc
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.