CVE-2026-87559
Overview
Background
- Tab fullscreen
- the mode entered via
Element.requestFullscreen()where the page fills the viewport and the browser toolbar is hidden, leaving no chrome to anchor UI to. - Cast dialog
- the Media Router (Chromecast) dialog opened by
MediaRouterDialogControllerViews::CreateMediaRouterDialog, normally anchored to the toolbar’s cast icon. - `ForSecurityDropFullscreen`
- a
WebContentsmethod that exits any active fullscreen and returns a scoped blocker (ScopedFullscreenBlocker) that prevents fullscreen re-entry for as long as the object is held. - `fullscreen_blocker_`
- the member on the dialog controller that stores that scoped blocker and is released in
Reset()when the dialog closes.
Root Cause Analysis
When opening the Cast dialog, CreateMediaRouterDialog is supposed to keep the page out of tab fullscreen, because there is no toolbar to anchor the dialog to in fullscreen and rendering it inside the content area is spoofable. The pre-patch code only acquired the fullscreen blocker when two conditions held at open time: an associated browser existed, and FullscreenController::IsTabFullscreen() already returned true. That made the protection a one-time snapshot: if the page was not fullscreen at the moment the dialog opened, no blocker was stored, so nothing stopped the page from calling requestFullscreen() afterward while the dialog remained visible.
The fix removes both conditions and calls initiator()->ForSecurityDropFullscreen(display::kInvalidDisplayId) unconditionally, storing the returned blocker in fullscreen_blocker_ (returning early if none is produced). Because the blocker now lives for the whole lifetime of the dialog and actively rejects new fullscreen requests, a page can no longer transition into tab fullscreen after the dialog is up.
Attack Path
- Open the Cast dialog without being fullscreen
A page (or user action) triggers
CreateMediaRouterDialogwhile the tab is in normal, windowed mode, soIsTabFullscreen()is false and the old code stores nofullscreen_blocker_. - Enter tab fullscreen after the dialog is showing
The page calls
document.documentElement.requestFullscreen(); with no blocker held, the request is granted and the toolbar disappears while the Cast dialog is still open. - Dialog renders without a trustworthy anchor With the toolbar gone, the dialog is no longer anchored to the cast icon and is presented within the content area the page controls.
- Spoof surrounding UI The page draws fake browser chrome or dialog content around/behind the genuine dialog in the fullscreen content area, misrepresenting the origin or intent of the UI to the user (inferred as the concrete spoofing payload; the patch only establishes that fullscreen entry becomes possible).
Impact Assessment
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
Audit Directions
- Snapshot vs. lifetime guardsLook for security UI (bubbles, prompts, dialogs) whose fullscreen/anchoring protection is applied only from the state observed at creation, rather than a scoped blocker held until the UI is dismissed.
- Conditional security dropsAudit other
ForSecurityDropFullscreencallers andExclusiveAccessManager/FullscreenControlleruses for early-outs gated onIsTabFullscreen()or a non-nullbrowser, which can leave the protection unarmed on legitimate paths. - Content-area-anchored UIReview browser-chrome UI that assumes a toolbar exists to anchor to, and verify each independently prevents post-show transitions into fullscreen or windowed modes that remove that anchor.
Patch
From c73d532470b3d530826dc5f8d8aea2135d9b172f Mon Sep 17 00:00:00 2001
From: Paul Jensen <pauljensen@chromium.org>
Date: Mon, 03 Aug 2026 06:35:52 -0700
Subject: [PATCH] [Media Router] Block tab fullscreen while Cast dialog is showing
CreateMediaRouterDialog only acquired a tab-fullscreen blocker when the
initiator was already tab-fullscreen at open time and had an associated
browser. Acquire the blocker unconditionally so the page also cannot
enter tab fullscreen after the dialog is shown, matching the
permission-prompt and chooser-bubble callers. The blocker is released in
Reset() as before.
Fixed: 514011926
Change-Id: I23839739489115dc16668e61135835ee8fbe8774
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8186138
Commit-Queue: Paul Jensen <pauljensen@chromium.org>
Auto-Submit: Paul Jensen <pauljensen@chromium.org>
Reviewed-by: Caroline Rising <corising@chromium.org>
Commit-Queue: Caroline Rising <corising@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672646}
---
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 569e0a0..3b4a81aa 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
@@ -72,20 +72,12 @@
// 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.
- if (browser) {
- ExclusiveAccessManager* exclusive_access_manager =
- ExclusiveAccessManager::From(browser);
- FullscreenController* fullscreen_controller =
- exclusive_access_manager->fullscreen_controller();
- if (fullscreen_controller->IsTabFullscreen()) {
- auto blocker =
- initiator()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
- if (!blocker) {
- return;
- }
- fullscreen_blocker_ = std::move(*blocker);
- }
+ auto blocker =
+ initiator()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
+ if (!blocker) {
+ return;
}
+ fullscreen_blocker_ = std::move(*blocker);
BrowserView* browser_view =
browser ? BrowserView::GetBrowserViewForBrowser(browser) : nullptr;
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 e454ac2..cffdbb8 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
@@ -2,24 +2,28 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "chrome/browser/ui/views/media_router/media_router_dialog_controller_views.h"
+
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
+#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/global_media_controls/media_dialog_view.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_coordinator.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_view.h"
-#include "chrome/browser/ui/views/media_router/media_router_dialog_controller_views.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/media_router/browser/media_router_metrics.h"
#include "components/media_router/browser/presentation/start_presentation_context.h"
#include "components/media_router/common/mojom/media_router.mojom.h"
#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/page_transition_types.h"
#include "ui/views/test/widget_test.h"
@@ -103,6 +107,12 @@
.GetCastDialogWidget();
ASSERT_TRUE(widget);
EXPECT_TRUE(widget->HasObserver(dialog_controller_));
+ FullscreenController* fullscreen_controller =
+ ExclusiveAccessManager::From(browser())->fullscreen_controller();
+ // While the dialog is showing, tab fullscreen cannot be entered.
+ EXPECT_FALSE(content::ExecJs(initiator_,
+ "document.documentElement.requestFullscreen()"));
+ EXPECT_FALSE(fullscreen_controller->IsTabFullscreen());
dialog_controller_->CloseMediaRouterDialog();
EXPECT_FALSE(dialog_controller_->IsShowingMediaRouterDialog());
EXPECT_EQ(dialog_controller_->GetCastDialogCoordinatorForTesting()
Regression Test / PoC
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 e454ac2..cffdbb8 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
@@ -2,24 +2,28 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "chrome/browser/ui/views/media_router/media_router_dialog_controller_views.h"
+
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
+#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/global_media_controls/media_dialog_view.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_coordinator.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_view.h"
-#include "chrome/browser/ui/views/media_router/media_router_dialog_controller_views.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/media_router/browser/media_router_metrics.h"
#include "components/media_router/browser/presentation/start_presentation_context.h"
#include "components/media_router/common/mojom/media_router.mojom.h"
#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/page_transition_types.h"
#include "ui/views/test/widget_test.h"
@@ -103,6 +107,12 @@
.GetCastDialogWidget();
ASSERT_TRUE(widget);
EXPECT_TRUE(widget->HasObserver(dialog_controller_));
+ FullscreenController* fullscreen_controller =
+ ExclusiveAccessManager::From(browser())->fullscreen_controller();
+ // While the dialog is showing, tab fullscreen cannot be entered.
+ EXPECT_FALSE(content::ExecJs(initiator_,
+ "document.documentElement.requestFullscreen()"));
+ EXPECT_FALSE(fullscreen_controller->IsTabFullscreen());
dialog_controller_->CloseMediaRouterDialog();
EXPECT_FALSE(dialog_controller_->IsShowingMediaRouterDialog());
EXPECT_EQ(dialog_controller_->GetCastDialogCoordinatorForTesting()
Original Bug Report
Potential UI spoofing in Cast dialog via fullscreen transition
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 Media Router (Cast) dialog fails to unconditionally acquire a fullscreen blocker, allowing a page to enter fullscreen mode after the dialog is requested. This hides the browser’s top-level UI and allows a malicious site to spoof the security context around the trusted dialog.
Affected files:
chrome/browser/ui/views/media_router/media_router_dialog_controller_views.ccchrome/browser/ui/views/media_router/cast_dialog_coordinator.ccchrome/browser/ui/views/toolbar/webui_pinned_toolbar_actions.cc
Estimated timestamp from git blame: 2026-04-13
A potential vulnerability exists in the Media Router dialog implementation where it fails to properly block fullscreen transitions, leading to possible UI spoofing.
In MediaRouterDialogControllerViews::CreateMediaRouterDialog (chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc), a fullscreen blocker is only acquired if the tab is already in fullscreen mode at the time the dialog is created:
if (browser) {
// ...
if (fullscreen_controller->IsTabFullscreen()) {
auto blocker =
initiator()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
if (!blocker) {
return;
}
fullscreen_blocker_ = std::move(*blocker);
}
}
Because this check is conditional on IsTabFullscreen(), a malicious page can request and enter fullscreen mode after the Cast dialog has been requested but before it is fully displayed, or even while it is already visible. When the tab enters fullscreen, the browser’s top-level chrome (including the Omnibox and security indicators) is hidden. The page can then render a spoofed browser environment (e.g., a fake Omnibox with a trusted origin) behind and around the legitimate Cast dialog.
This is particularly concerning for the Cast dialog’s default ‘Cast tab’ mode, which does not display the origin of the tab being cast, relying instead on the browser’s Omnibox for origin verification. An attacker could trick a user into believing they are casting a trusted site while they are actually interacting with a malicious one.
Furthermore, the ‘Anchor Fallback’ path (lines 108-117), which is used when no browser window is found for the initiator, skips the fullscreen blocker logic entirely, despite a comment in the source code (line 73) explicitly noting that showing the dialog in the content area without a toolbar is ‘susceptible to spoofing attacks.’
Potential Reproduction Steps (Suggested)
- Navigate to a malicious page that requests fullscreen after a short delay (e.g., using
setTimeout). - Interact with the page (e.g., right-click) to grant it transient user activation.
- Open the Cast dialog via the context menu or browser toolbar.
- If the fullscreen request executes after the dialog is requested but while no blocker is active, the browser will grant fullscreen and hide the trusted UI.
- Observe the Cast dialog displayed over an attacker-controlled fullscreen surface with spoofed browser chrome.
Note: These steps are based on code analysis and have not been verified with a working proof of concept.
Suggested Fix
Call ForSecurityDropFullscreen unconditionally in CreateMediaRouterDialog to ensure a blocker is always acquired for the lifetime of the dialog, regardless of the initial fullscreen state or the anchoring method. This aligns with the implementation used by other security-sensitive components, such as the File System Access API.
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.