CVE-2025-12444
Overview
Background
- Content (tab) fullscreen
- the mode entered via the Fullscreen API (
requestFullscreen()), in which the page fills the screen and Chrome hides the toolbar, omnibox and tab strip, as reported byFullscreenController::IsTabFullscreen(). - Permission prompt bubble
- the browser-drawn bubble (
PermissionPromptBubble, which builds aPermissionPromptBubbleBaseView) that asks the user to allow or block a permission such as geolocation for the requesting origin. - `ForSecurityDropFullscreen`
- a
WebContentsAPI that exits content fullscreen and returns abase::ScopedClosureRunner; while that runner is alive, the tab cannot enter fullscreen again. - Security UI spoofing
- tricking the user about which origin or context a trusted browser surface belongs to, usually by drawing fake browser chrome around a real prompt.
Root Cause Analysis
Before the patch, PermissionPromptBubble::ShowBubble() created and showed the permission bubble without checking fullscreen state. A page in content fullscreen could therefore have a permission prompt drawn over a screen it fully controls, with no real omnibox or toolbar visible. The commit message says this “is prone to spoofing attacks”: the page can paint a fake address bar or other browser chrome around the real bubble, so the user may misjudge which site is asking or what the prompt means.
The fix adds a check at the start of ShowBubble(): if fullscreen_controller->IsTabFullscreen() is true, it calls web_contents()->ForSecurityDropFullscreen(display::kInvalidDisplayId) and stores the returned base::ScopedClosureRunner in a new member, fullscreen_blocker_. This both exits fullscreen and stops the page from re-entering it while the bubble object exists. The new test confirms both halves. requestFullscreen() is rejected while the bubble is up, and succeeds again once the request is accepted and the bubble closes.
The fix only tightens the code: it adds a precondition and changes no existing check.
ForSecurityDropFullscreen blocker for as long as the bubble exists.Attack Path
- Enter content fullscreen
A web page calls
document.documentElement.requestFullscreen(), which normally needs a user gesture, and the toolbar and omnibox disappear. - Draw fake browser chrome The fullscreen page paints its own imitation of the browser UI, such as a fake address bar showing a trusted origin (inferred; the commit message only says the setup is “prone to spoofing”).
- Trigger a permission request
The page requests a permission such as geolocation, the request used in the regression test, and
PermissionPromptBubble::ShowBubble()shows the real bubble over the fullscreen content. - Mislead the user's decision Because the real browser UI is missing, the user reads the bubble in the context of the fake chrome and may grant or deny the permission based on a false idea of who is asking (inferred).
Impact Assessment
Files Changed
chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.ccchrome/browser/ui/views/permissions/permission_prompt_bubble.ccchrome/browser/ui/views/permissions/permission_prompt_bubble.h
Audit Directions
- Trusted bubbles shown over fullscreen contentReview other browser-drawn prompts and dialogs (choosers, download warnings, Web Authentication, file pickers, payment sheets) for whether they drop or block content fullscreen before showing, as
PermissionPromptBubblenow does. - Blocker lifetime vs. UI lifetimeCheck that each
ForSecurityDropFullscreenrunner (or similar blocker) lives exactly as long as the sensitive UI is visible. Look for bubbles that are hidden, re-anchored or recreated (tab switches, chip-to-bubble transitions, other prompt styles) without the blocker being kept or re-acquired. - Prompt styles that skip `ShowBubble`Check whether other permission prompt presentations (for example, chip-based or other
PermissionPromptStylepaths) bypass this function and can still appear over fullscreen content.
Patch
From 5eb4b823dced000b8d61678f1c885f38c2dc4fbe Mon Sep 17 00:00:00 2001 From: Keren Zhu <[email protected]> Date: Mon, 08 Sep 2025 10:57:48 -0700 Subject: [PATCH] Exit content fullscreen when a permission prompt bubble is shown. In content fullscreen there is almost no browser UI. Showing a permission prompt bubble is therefore prone to spoofing attacks. This CL exits the content fullscreen on showing a permission bubble. It also prevents re-entering content fullscreen while the permission prompt bubble is visible. Fixed: 390571618 Change-Id: I87c1a58ebd68a39d0beda8a5cf056de89f92d679 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6921231 Reviewed-by: Tom Lukaszewicz <[email protected]> Commit-Queue: Keren Zhu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1512541} --- diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc index 40e862f9..35fb551 100644 --- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc +++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc @@ -34,6 +34,7 @@ #include "components/content_settings/core/browser/host_content_settings_map.h" #include "components/metrics/content/subprocess_metrics_provider.h" #include "components/permissions/permission_request_manager.h" +#include "components/permissions/test/mock_permission_request.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/render_widget_host_view.h" @@ -745,6 +746,48 @@ } } +// Tests that showing a permission prompt bubble exits tab fullscreen. +IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest, + PermissionPromptExitsTabFullscreen) { + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank"))); + content::WebContents* web_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + FullscreenController* fullscreen_controller = browser() + ->GetFeatures() + .exclusive_access_manager() + ->fullscreen_controller(); + + // Enter tab fullscreen. + ToggleTabFullscreen(true); + ui_test_utils::FullscreenWaiter(browser(), {.tab_fullscreen = true}).Wait(); + + // Request a permission to show the bubble, which should exit fullscreen. + permissions::PermissionRequestManager* permission_request_manager = + permissions::PermissionRequestManager::FromWebContents(web_contents); + permission_request_manager->AddRequest( + web_contents->GetPrimaryMainFrame(), + std::make_unique<permissions::MockPermissionRequest>( + permissions::RequestType::kGeolocation)); + + ui_test_utils::FullscreenWaiter(browser(), {.tab_fullscreen = false}).Wait(); + ASSERT_FALSE(fullscreen_controller->IsTabFullscreen()); + + // While bubble is showing, tab fullscreen cannot be entered. + EXPECT_THAT(content::EvalJs(web_contents, + "document.documentElement.requestFullscreen()"), + content::EvalJsResult::IsError()); + ASSERT_FALSE(fullscreen_controller->IsTabFullscreen()); + + // Accept the permission request to close the bubble. + permission_request_manager->Accept(); + + // Now we should be able to enter tab fullscreen again. + EXPECT_THAT(content::EvalJs(web_contents, + "document.documentElement.requestFullscreen()"), + content::EvalJsResult::IsOk()); + ASSERT_TRUE(fullscreen_controller->IsTabFullscreen()); +} + // Tests ToggleFullscreenModeForTab always causes window to change. IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest, ToggleFullscreenModeForTab) { @@ -1276,11 +1319,15 @@ auto* tab = browser()->tab_strip_model()->GetActiveWebContents(); - // Auto-accept Window Management permission prompts. - permissions::PermissionRequestManager* permission_request_manager = - permissions::PermissionRequestManager::FromWebContents(tab); - permission_request_manager->set_auto_response_for_test( - permissions::PermissionRequestManager::ACCEPT_ALL); + // Grant Window Management permission prompts. + // Don't use PermissionRequestManager::set_auto_response_for_test() because + // it shows the permission bubble before granting the permission, which will + // cause content fullscreen to exit due to security reason. + auto* content_settings = + HostContentSettingsMapFactory::GetForProfile(browser()->profile()); + content_settings->SetContentSettingDefaultScope( + url, GURL(), ContentSettingsType::WINDOW_MANAGEMENT, + CONTENT_SETTING_ALLOW); return tab; } diff --git a/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc b/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc index b1286e5..8aa8c90 100644 --- a/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc +++ b/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc @@ -6,12 +6,16 @@ #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" +#include "chrome/browser/ui/browser_window/public/browser_window_features.h" +#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h" +#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h" #include "chrome/browser/ui/views/location_bar/location_bar_view.h" #include "chrome/browser/ui/views/permissions/permission_prompt_bubble_base_view.h" #include "chrome/browser/ui/views/permissions/permission_prompt_bubble_view_factory.h" #include "chrome/browser/ui/views/permissions/permission_prompt_style.h" #include "components/permissions/features.h" #include "content/public/browser/web_contents.h" +#include "ui/display/types/display_constants.h" PermissionPromptBubble::PermissionPromptBubble( Browser* browser, @@ -36,6 +40,16 @@ } void PermissionPromptBubble::ShowBubble() { + FullscreenController* fullscreen_controller = browser() + ->GetFeatures() + .exclusive_access_manager() + ->fullscreen_controller(); + CHECK(fullscreen_controller); + if (fullscreen_controller->IsTabFullscreen()) { + fullscreen_blocker_ = + web_contents()->ForSecurityDropFullscreen(display::kInvalidDisplayId); + } + raw_ptr<PermissionPromptBubbleBaseView> prompt_bubble = CreatePermissionPromptBubbleView(browser(), delegate()->GetWeakPtr(), PermissionPromptStyle::kBubbleOnly); diff --git a/chrome/browser/ui/views/permissions/permission_prompt_bubble.h b/chrome/browser/ui/views/permissions/permission_prompt_bubble.h index ba10ff62..3e60b72 100644 --- a/chrome/browser/ui/views/permissions/permission_prompt_bubble.h +++ b/chrome/browser/ui/views/permissions/permission_prompt_bubble.h @@ -53,6 +53,11 @@ base::ScopedClosureRunner disallowed_custom_cursors_scope_; + // Used to prevent the tab from entering content fullscreen mode while the + // permission prompt bubble is visible. The content fullscreen mode has no + // browser toolbar UI and therefore is prone to spoofing attacks. + base::ScopedClosureRunner fullscreen_blocker_; + base::WeakPtrFactory<PermissionPromptBubble> weak_factory_{this}; };
Regression Test / PoC
diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
index 40e862f9..35fb551 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -34,6 +34,7 @@
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/metrics/content/subprocess_metrics_provider.h"
#include "components/permissions/permission_request_manager.h"
+#include "components/permissions/test/mock_permission_request.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
@@ -745,6 +746,48 @@
}
}
+// Tests that showing a permission prompt bubble exits tab fullscreen.
+IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
+ PermissionPromptExitsTabFullscreen) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")));
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ FullscreenController* fullscreen_controller = browser()
+ ->GetFeatures()
+ .exclusive_access_manager()
+ ->fullscreen_controller();
+
+ // Enter tab fullscreen.
+ ToggleTabFullscreen(true);
+ ui_test_utils::FullscreenWaiter(browser(), {.tab_fullscreen = true}).Wait();
+
+ // Request a permission to show the bubble, which should exit fullscreen.
+ permissions::PermissionRequestManager* permission_request_manager =
+ permissions::PermissionRequestManager::FromWebContents(web_contents);
+ permission_request_manager->AddRequest(
+ web_contents->GetPrimaryMainFrame(),
+ std::make_unique<permissions::MockPermissionRequest>(
+ permissions::RequestType::kGeolocation));
+
+ ui_test_utils::FullscreenWaiter(browser(), {.tab_fullscreen = false}).Wait();
+ ASSERT_FALSE(fullscreen_controller->IsTabFullscreen());
+
+ // While bubble is showing, tab fullscreen cannot be entered.
+ EXPECT_THAT(content::EvalJs(web_contents,
+ "document.documentElement.requestFullscreen()"),
+ content::EvalJsResult::IsError());
+ ASSERT_FALSE(fullscreen_controller->IsTabFullscreen());
+
+ // Accept the permission request to close the bubble.
+ permission_request_manager->Accept();
+
+ // Now we should be able to enter tab fullscreen again.
+ EXPECT_THAT(content::EvalJs(web_contents,
+ "document.documentElement.requestFullscreen()"),
+ content::EvalJsResult::IsOk());
+ ASSERT_TRUE(fullscreen_controller->IsTabFullscreen());
+}
+
// Tests ToggleFullscreenModeForTab always causes window to change.
IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
ToggleFullscreenModeForTab) {
@@ -1276,11 +1319,15 @@
auto* tab = browser()->tab_strip_model()->GetActiveWebContents();
- // Auto-accept Window Management permission prompts.
- permissions::PermissionRequestManager* permission_request_manager =
- permissions::PermissionRequestManager::FromWebContents(tab);
- permission_request_manager->set_auto_response_for_test(
- permissions::PermissionRequestManager::ACCEPT_ALL);
+ // Grant Window Management permission prompts.
+ // Don't use PermissionRequestManager::set_auto_response_for_test() because
+ // it shows the permission bubble before granting the permission, which will
+ // cause content fullscreen to exit due to security reason.
+ auto* content_settings =
+ HostContentSettingsMapFactory::GetForProfile(browser()->profile());
+ content_settings->SetContentSettingDefaultScope(
+ url, GURL(), ContentSettingsType::WINDOW_MANAGEMENT,
+ CONTENT_SETTING_ALLOW);
return tab;
}
Original Bug Report
Misuse of Permission Dialog Dismiss to Deceive Users About Fullscreen Status
Steps to reproduce the problem
- Victim visit: https://syarifmsajjad.github.io/test-spoofing/fs-escape.html and click to enter fullscreen
- Press “Esc”. “Insert omnibox here” text should appear. An attacker can use this opportunity to trick the user into thinking that they have exited fullscreen and draw a fake omnibox once the user pressed “Esc”
Problem Description
In Chrome, the permission dialog can be closed using the “Esc” key, which also serves to exit fullscreen mode. When the permission dialog appears after a website takes the user into fullscreen mode, if the user presses “Esc” afterwards, the input will be redirected to the permission dialog instead of exiting fullscreen. This allows attackers to insert a fake omnibox and trick users into thinking that they have exited fullscreen mode. With this fake omnibox, the attacker can perform URL spoofing of other websites.
Expected result Pressing the “Esc” key while in fullscreen mode with a permission dialog open should exit fullscreen mode without redirecting input to the permission dialog. The browser should prioritize exiting fullscreen mode to ensure consistent and secure user experience, preventing potential UI spoofing vulnerabilities.
Actual result When the victim presses the “Esc” key while the permission dialog is displayed in fullscreen mode, instead of exiting fullscreen, the input is redirected to the permission dialog. This allows the attacker to insert a fake omnibox, tricking the user into believing they have exited fullscreen mode.
Summary
Misuse of Permission Dialog Dismiss to Deceive Users About Fullscreen Status
Custom Questions
Type of crash:
1
Crash state:
1
Reporter credit:
syrf
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A