Low watch CVSS 4.2 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Fullscreen UI
DescriptionIncorrect security UI in Fullscreen UI
ComponentFullscreen UI
Sub-componentUI>Browser>FullScreen
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker390571618
Fix commit5eb4b823dced (chromium/src) +71/-5
CWECWE-306
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
Creditedsyrf
Reported2025-01-18 · bounty $3000 (per vendor advisory)
Fix landed2025-09-08 (public git)
Disclosed2025-10-28
Patch gap50 days — fix public in source before the advisory

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 by FullscreenController::IsTabFullscreen().
Permission prompt bubble
the browser-drawn bubble (PermissionPromptBubble, which builds a PermissionPromptBubbleBaseView) that asks the user to allow or block a permission such as geolocation for the requesting origin.
`ForSecurityDropFullscreen`
a WebContents API that exits content fullscreen and returns a base::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.

Key insight
Permission bubbles could appear while the page owned the entire screen in content fullscreen, so the real browser UI that identifies the requesting origin was missing. The fix exits fullscreen when the bubble is shown and holds a ForSecurityDropFullscreen blocker for as long as the bubble exists.

Attack Path

  1. Enter content fullscreen A web page calls document.documentElement.requestFullscreen(), which normally needs a user gesture, and the toolbar and omnibox disappear.
  2. 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”).
  3. 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.
  4. 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

The attacker gets a misleading security UI, not memory corruption. They can show a genuine permission prompt in a context they fully control, which may lead the user to grant a permission they would otherwise refuse. Ordinary web content in a stock desktop (Views) build can reach this. The preconditions are a user gesture to enter fullscreen (a standard Fullscreen API requirement) and the user’s interaction with the prompt; no compromised renderer, flag or extension is needed. The vendor rates the issue Low severity (“Incorrect security UI in Fullscreen UI”), and the provided metadata includes no exploited-in-the-wild statement.

Files Changed

  • chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
  • chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
  • chrome/browser/ui/views/permissions/permission_prompt_bubble.h

Audit Directions

  • Trusted bubbles shown over fullscreen content
    Review 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 PermissionPromptBubble now does.
  • Blocker lifetime vs. UI lifetime
    Check that each ForSecurityDropFullscreen runner (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 PermissionPromptStyle paths) bypass this function and can still appear over fullscreen content.
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};
 };
 
Loading diff…

Regression Test / PoC

shipped with the fix
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;
   }
Loading diff…

Original Bug Report

reported by [email protected]

Misuse of Permission Dialog Dismiss to Deceive Users About Fullscreen Status

Steps to reproduce the problem

  1. Victim visit: https://syarifmsajjad.github.io/test-spoofing/fs-escape.html and click to enter fullscreen
  2. 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

View on issue tracker