CVE-2026-13996
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/permissions/permission_prompt_bubble.cc |
modified |
Files Changed
chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.ccchrome/browser/ui/views/permissions/permission_prompt_bubble.cc
Patch
From 0928fe90fac941dbd4d57feeb6a88aee0fcfdb58 Mon Sep 17 00:00:00 2001
From: Antonio Sartori <antoniosartori@chromium.org>
Date: Tue, 19 May 2026 00:13:55 -0700
Subject: [PATCH] [permissions] Prevent fullscreen while prompt is shown
Instead of just exiting fullscreen if a permission prompt is triggered
in fullscreen, let's prevent fullescreen altogether while a permission
prompt is shown.
Change-Id: Ie989050d4959af8a6dd084f5f70e7ee3117ebbcb
Bug: 514068972
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7849423
Reviewed-by: Keren Zhu <kerenzhu@chromium.org>
Commit-Queue: Antonio Sartori <antoniosartori@chromium.org>
Reviewed-by: Muyao Xu <muyaoxu@google.com>
Cr-Commit-Position: refs/heads/main@{#1632664}
---
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 a1733d03..64aaceb 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -39,6 +39,7 @@
#include "components/permissions/fake_usb_chooser_controller.h"
#include "components/permissions/permission_request_manager.h"
#include "components/permissions/test/mock_permission_request.h"
+#include "components/permissions/test/permission_request_observer.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"
@@ -812,6 +813,45 @@
ASSERT_TRUE(fullscreen_controller->IsTabFullscreen());
}
+// Tests that fullscreen cannot be entered while a permission prompt bubble
+// exits.
+IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
+ PermissionPromptPreventsTabFullscreen) {
+ 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();
+
+ permissions::PermissionRequestObserver observer(web_contents);
+
+ // Request a permission to show the bubble.
+ 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));
+
+ observer.Wait();
+ ASSERT_TRUE(observer.request_shown());
+
+ // While bubble is showing, tab fullscreen cannot be entered.
+ EXPECT_FALSE(content::ExecJs(web_contents,
+ "document.documentElement.requestFullscreen()"));
+ ASSERT_FALSE(fullscreen_controller->IsTabFullscreen());
+
+ // Accept the permission request to close the bubble.
+ permission_request_manager->Accept(/*prompt_options=*/std::monostate());
+
+ // Now we should be able to enter tab fullscreen again.
+ EXPECT_TRUE(content::ExecJs(web_contents,
+ "document.documentElement.requestFullscreen()"));
+ ASSERT_TRUE(fullscreen_controller->IsTabFullscreen());
+}
+
// Tests that showing a chooser bubble exits tab fullscreen.
// TODO(http://crbug.com/493319451): Re-enable when the flakiness is fixed.
IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
diff --git a/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc b/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
index f176aab..e3d44df 100644
--- a/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
+++ b/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
@@ -6,9 +6,6 @@
#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"
@@ -40,19 +37,12 @@
}
void PermissionPromptBubble::ShowBubble() {
- FullscreenController* fullscreen_controller = browser()
- ->GetFeatures()
- .exclusive_access_manager()
- ->fullscreen_controller();
- CHECK(fullscreen_controller);
- if (fullscreen_controller->IsTabFullscreen()) {
auto blocker =
web_contents()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
if (!blocker) {
return;
}
fullscreen_blocker_ = std::move(*blocker);
- }
raw_ptr<PermissionPromptBubbleBaseView> prompt_bubble =
CreatePermissionPromptBubbleView(browser(), delegate()->GetWeakPtr(),
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 a1733d03..64aaceb 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -39,6 +39,7 @@
#include "components/permissions/fake_usb_chooser_controller.h"
#include "components/permissions/permission_request_manager.h"
#include "components/permissions/test/mock_permission_request.h"
+#include "components/permissions/test/permission_request_observer.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"
@@ -812,6 +813,45 @@
ASSERT_TRUE(fullscreen_controller->IsTabFullscreen());
}
+// Tests that fullscreen cannot be entered while a permission prompt bubble
+// exits.
+IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
+ PermissionPromptPreventsTabFullscreen) {
+ 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();
+
+ permissions::PermissionRequestObserver observer(web_contents);
+
+ // Request a permission to show the bubble.
+ 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));
+
+ observer.Wait();
+ ASSERT_TRUE(observer.request_shown());
+
+ // While bubble is showing, tab fullscreen cannot be entered.
+ EXPECT_FALSE(content::ExecJs(web_contents,
+ "document.documentElement.requestFullscreen()"));
+ ASSERT_FALSE(fullscreen_controller->IsTabFullscreen());
+
+ // Accept the permission request to close the bubble.
+ permission_request_manager->Accept(/*prompt_options=*/std::monostate());
+
+ // Now we should be able to enter tab fullscreen again.
+ EXPECT_TRUE(content::ExecJs(web_contents,
+ "document.documentElement.requestFullscreen()"));
+ ASSERT_TRUE(fullscreen_controller->IsTabFullscreen());
+}
+
// Tests that showing a chooser bubble exits tab fullscreen.
// TODO(http://crbug.com/493319451): Re-enable when the flakiness is fixed.
IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
Original Bug Report
UI spoofing in PermissionPromptBubble 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: A logic flaw in PermissionPromptBubble allows a webpage to enter fullscreen mode after a permission prompt is displayed. An attacker can use this to hide the browser’s native Omnibox and display spoofed UI underneath the legitimate permission bubble. This occurs because the prompt fails to acquire a fullscreen blocker if it is initially shown while the tab is in windowed mode.
Affected files:
chrome/browser/ui/views/permissions/permission_prompt_bubble.ccchrome/browser/ui/views/permissions/permission_prompt_base_view.ccchrome/browser/ui/views/permissions/permission_prompt_bubble_base_view.ccchrome/browser/ui/views/permissions/permission_prompt_base_view.h
Estimated timestamp from git blame: 2025-09-08
Summary
A logic flaw in PermissionPromptBubble::ShowBubble() allows a webpage to transition into fullscreen mode after a permission prompt has been displayed. This enables an attacker to hide the browser’s native Omnibox and UI, replacing it with spoofed content while the legitimate permission prompt remains visible and re-anchored to the fullscreen window. This facilitates a UI dressing attack where a user may be misled into granting permissions to an attacker’s origin, believing it to be a trusted site.
Technical Analysis
In chrome/browser/ui/views/permissions/permission_prompt_bubble.cc, the fullscreen_blocker_ (a base::ScopedClosureRunner that prevents fullscreen transitions) is only acquired if the tab is already in fullscreen at the moment the bubble is shown:
// chrome/browser/ui/views/permissions/permission_prompt_bubble.cc:48
if (fullscreen_controller->IsTabFullscreen()) {
auto blocker =
web_contents()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
if (!blocker) {
return;
}
fullscreen_blocker_ = std::move(*blocker);
}
Because the blocker is only initialized when IsTabFullscreen() is true, permission requests that trigger a bubble while in windowed mode (e.g., Geolocation requests on page load without a user gesture) do not increment the WebContentsImpl::fullscreen_blocker_count_.
When a site subsequently requests fullscreen (following a user gesture), WebContentsImpl::CanEnterFullscreenMode() is invoked. This check passes because the fullscreen_blocker_count_ is zero:
// content/browser/web_contents/web_contents_impl.cc:4763
return delegate_ &&
std::ranges::all_of(GetAllOpeningWebContents(this),
[](auto opener) {
return opener->fullscreen_blocker_count_ == 0;
}) &&
delegate_->CanEnterFullscreenModeForTab(requesting_frame);
Upon entering fullscreen, the browser toolbar is hidden. During the subsequent layout pass, PermissionPromptBubble::UpdateAnchor() is called, which invokes AnchorToPageInfoOrChip(). In chrome/browser/ui/views/permissions/permission_prompt_base_view.cc, this method explicitly handles the null-anchor case (caused by the hidden toolbar) by anchoring the bubble to a default rect at the top of the screen:
// chrome/browser/ui/views/permissions/permission_prompt_base_view.cc:155
if (configuration.anchor.IsNull()) {
SetAnchorRect(bubble_anchor_util::GetPageInfoAnchorRect(browser_));
}
This ensures the legitimate bubble remains visible and correctly positioned for an attacker to render a fake Omnibox directly underneath it using fullscreen content.
Potential Reproduction Steps
- A user navigates to
https://attacker.test. - The page immediately calls
navigator.geolocation.getCurrentPosition(). Since this is often allowed without an initial gesture (andfeatures::kPermissionsGestureGatedPromptsis disabled by default), Chrome displays thePermissionPromptBubbleanchored to the padlock icon. - The page displays a button to entice the user to click (e.g., “Enter Site”).
- The user’s click provides a gesture, allowing the page to call
document.documentElement.requestFullscreen(). - The browser enters fullscreen mode, hiding the real Omnibox.
- The geolocation bubble remains visible and moves to the top of the screen.
- The attacker’s page renders a fake address bar showing
https://accounts.google.combehind the real bubble. - The user, believing the context of the fake address bar, clicks “Allow” on the legitimate prompt.
Note: These steps are based on source code analysis and have not been verified with a running proof-of-concept.
Suggested Fix
PermissionPromptBubble::ShowBubble() should always acquire a fullscreen_blocker_ regardless of the current fullscreen state. This ensures that as long as a permission bubble is visible, the tab is prevented from entering content fullscreen mode.
// Suggested change in chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
auto blocker = web_contents()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
if (blocker) {
fullscreen_blocker_ = std::move(*blocker);
}
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.