Medium watch CVSS 5.4 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Fullscreen
DescriptionIncorrect security UI in Fullscreen
ComponentFullscreen
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.
Tracker343352552
Fix commitfdac21de070f (chromium/src) +95/-10
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedWolfgang Ettlinger (aff. Certitude Consulting GmbH) Alexander Hurbean (aff. Certitude Consulting GmbH)
Reported2024-05-29 (per vendor advisory)
Fix landed2026-03-31 (public git)
Disclosed2026-05-12
Patch gap42 days — fix public in source before the advisory

Background

Exclusive access bubble
the transient fullscreen/pointer-lock notification (“Press Esc to exit full screen”) that must stay visible long enough for the user to read it before auto-hiding.
`hide_timeout_`
the timer that auto-hides the bubble after kShowTime; it is started when the bubble is shown.
`snooze_until_`
the deadline controlling when the bubble may re-show after user input, set in the Snooze() logic.
Presentation commit
the point at which a frame is actually displayed on screen (tracked via viz FrameTimingDetails), which can lag behind the code that requests the bubble be shown.

Root Cause Analysis

The auto-hide timer was started at the moment the code asked to show the bubble rather than when the bubble was actually presented, so on a slow present the hide_timeout_ could count down (or expire) while the notification was not yet visible, shortening or eliminating the window in which the user sees the fullscreen security UI.

The patch refactors exclusive_access_bubble.cc so ShowAndStartTimers() calls Show() then the new StartHideTimer() (which does hide_timeout_.Reset() then Snooze()), and factors the snooze-deadline update into a dedicated Snooze(); OnUserInput() now re-shows via ShowAndStartTimers() without the old early return and always calls Snooze(). Crucially, ShowAndStartTimers() is made virtual (header change), allowing the Views implementation in exclusive_access_bubble_views.cc — which now includes components/viz/common/frame_timing_details.h — to override it and defer StartHideTimer() until the frame is committed/presented (the override body itself is not shown in this diff, so the commit-time wiring is inferred).

The result is that the hide timer only begins once the bubble is genuinely on screen, so the notification is displayed for its full intended duration; the browsertest change Wait(ExclusiveAccessBubble::kShowTime * 2) reflects the added presentation delay.

Key insight
The core mistake was starting the auto-hide timer at show-request time instead of at presentation-commit time, letting the fullscreen notification be hidden before the user could see it; the fix splits out StartHideTimer()/Snooze() and makes ShowAndStartTimers() virtual so the Views bubble can defer the hide timer until the frame commits.

Attack Path

  1. Enter fullscreen An attacker page requests fullscreen (or pointer lock), triggering the exclusive-access bubble that warns the user how to exit.
  2. Race the presentation Because the hide_timeout_ started before the bubble was actually presented, under presentation delay the timer elapses while the bubble is not yet (or only briefly) visible.
  3. Notification missed The security UI auto-hides before the user can perceive it, so the user is unaware they are in fullscreen or how to leave.
  4. UI spoofing With the warning effectively suppressed, the fullscreen page can draw fake browser chrome or security indicators and mislead the user (incorrect security UI / spoofing, as the advisory describes).

Impact Assessment

An attacker-controlled page entering fullscreen could cause the exit/notification bubble to hide before the user reliably sees it, enabling spoofing of browser UI within fullscreen in the browser-process UI. The advisory rates this medium severity (incorrect security UI in Fullscreen); it is a UI/spoofing issue with no memory-safety consequence, and no in-the-wild status is stated. Precondition: the user is induced to enter fullscreen on an attacker page.

Changed Functions

FunctionChangeNotes
ExclusiveAccessBubbleTest
chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
modified
TEST_F
chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
modified
FullscreenControllerInteractiveTest
chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
  • chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
  • chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
  • chrome/browser/ui/exclusive_access/exclusive_access_test.cc
  • chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
  • chrome/browser/ui/views/exclusive_access_bubble_views.cc

Audit Directions

  • Timers started before display
    Audit any security/notification UI whose auto-hide or validity timer starts at request time rather than at actual presentation/commit, especially where present can be delayed.
  • Fullscreen/exclusive-access notifications
    Verify every exclusive-access or permission bubble guarantees a minimum on-screen visibility independent of rendering latency.
  • Early-return timer bugs
    Review input/snooze handlers where an early return skips refreshing a deadline, which can leave the UI in an inconsistent show/hide state.
From fdac21de070f3523b7315a268092d528a542a6d6 Mon Sep 17 00:00:00 2001
From: Frank Liberato <[email protected]>
Date: Tue, 31 Mar 2026 16:30:44 -0700
Subject: [PATCH] Don't start the exclusive access bubble timer until commit

Bug: 343352552
Change-Id: If0f8704a23c51e1431f9919ea88b989f74de4a4c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7615417
Reviewed-by: Mike Wasserman <[email protected]>
Commit-Queue: Frank Liberato <[email protected]>
Reviewed-by: Muyao Xu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1608174}
---

diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
index 3df48a00..3bc3c10 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
@@ -21,19 +21,22 @@
   // Re-show the bubble if no user input occurred during the snooze period.
   if (base::TimeTicks::Now() > snooze_until_) {
     ShowAndStartTimers();
-    return;
   }
 
-  // Restart the snooze period; to only re-show after a period of inactivity.
-  snooze_until_ = base::TimeTicks::Now() + kSnoozeTime;
+  Snooze();
 }
 
 void ExclusiveAccessBubble::ShowAndStartTimers() {
   Show();
+  StartHideTimer();
+}
 
-  // Restart the timer to hide the bubble after a few seconds.
+void ExclusiveAccessBubble::StartHideTimer() {
   hide_timeout_.Reset();
+  Snooze();
+}
 
+void ExclusiveAccessBubble::Snooze() {
   // Restart the snooze period; to only re-show after a period of inactivity.
   snooze_until_ = base::TimeTicks::Now() + kSnoozeTime;
 }
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
index 0051d623..376bae87 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
@@ -37,7 +37,13 @@
   virtual void Show() = 0;
 
   // Shows the bubble and sets up timers to auto-hide and snooze.
-  void ShowAndStartTimers();
+  virtual void ShowAndStartTimers();
+
+  // Starts the timer to hide the bubble after a short time.
+  void StartHideTimer();
+
+  // Reset the timeout for user input before we auto-show again.
+  void Snooze();
 
   // Cached content and traits for this bubble.
   ExclusiveAccessBubbleParams params_;
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
index 5b87012b..a129897 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
@@ -13,12 +13,13 @@
   explicit MockExclusiveAccessBubble(ExclusiveAccessBubbleParams params)
       : ExclusiveAccessBubble(params) {}
   ~MockExclusiveAccessBubble() override = default;
-  MOCK_METHOD(void, Hide, (), (override));
   MOCK_METHOD(void, Show, (), (override));
+  MOCK_METHOD(void, Hide, (), (override));
 
   using ExclusiveAccessBubble::hide_timeout_;
   using ExclusiveAccessBubble::ShowAndStartTimers;
   using ExclusiveAccessBubble::snooze_until_;
+  using ExclusiveAccessBubble::StartHideTimer;
 };
 
 class ExclusiveAccessBubbleTest : public testing::Test {
@@ -63,3 +64,20 @@
   task_environment_.FastForwardBy(base::Minutes(16));
   bubble_.OnUserInput();
 }
+
+TEST_F(ExclusiveAccessBubbleTest, StartHideTimerRestartsTimer) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  EXPECT_TRUE(bubble_.hide_timeout_.IsRunning());
+
+  task_environment_.FastForwardBy(base::Seconds(1));
+  auto remaining =
+      bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+  EXPECT_LT(remaining, ExclusiveAccessBubble::kShowTime);
+
+  // Verify that the time increases.
+  bubble_.StartHideTimer();
+  auto new_remaining =
+      bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+  EXPECT_LT(remaining, new_remaining);
+}
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
index 2756a83..91fd829 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
@@ -74,6 +74,8 @@
 ExclusiveAccessTest::~ExclusiveAccessTest() = default;
 
 void ExclusiveAccessTest::SetUpOnMainThread() {
+  ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(true);
+
   permission_controller_ =
       std::make_unique<content::MockPermissionController>();
   ON_CALL(*permission_controller_, RequestPermissionsFromCurrentDocument)
@@ -110,6 +112,8 @@
 }
 
 void ExclusiveAccessTest::TearDownOnMainThread() {
+  ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(false);
+
   GetExclusiveAccessManager()
       ->pointer_lock_controller()
       ->bubble_hide_callback_for_test_ =
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 e34c562..0245179 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -80,13 +80,13 @@
 }  // namespace
 
 class FullscreenControllerInteractiveTest : public ExclusiveAccessTest {
+ protected:
   void SetUpOnMainThread() override {
     ExclusiveAccessTest::SetUpOnMainThread();
 
     SetDisableFullscreenWithinTab(true);
   }
 
- protected:
   void SetUpCommandLine(base::CommandLine* command_line) override {
     ExclusiveAccessTest::SetUpCommandLine(command_line);
     // Slow bots are flaky due to slower loading interacting with
@@ -981,6 +981,7 @@
   }
 
   void SetUpOnMainThread() override {
+    FullscreenControllerInteractiveTest::SetUpOnMainThread();
     auto allow_automatic_fullscreen = [&](const GURL& url) {
       HostContentSettingsMapFactory::GetForProfile(browser()->profile())
           ->SetContentSettingDefaultScope(
@@ -1014,7 +1015,10 @@
     ASSERT_TRUE(WaitForRenderFrameReady(web_contents_->GetPrimaryMainFrame()));
   }
 
-  void TearDownOnMainThread() override { web_contents_ = nullptr; }
+  void TearDownOnMainThread() override {
+    web_contents_ = nullptr;
+    FullscreenControllerInteractiveTest::TearDownOnMainThread();
+  }
 
   bool RequestFullscreen(bool gesture = false,
                          content::RenderFrameHost* rfh = nullptr) {
@@ -1327,6 +1331,7 @@
     : public FullscreenControllerInteractiveTest {
  public:
   void SetUpOnMainThread() override {
+    FullscreenControllerInteractiveTest::SetUpOnMainThread();
     if (!SetUpVirtualDisplays()) {
       GTEST_SKIP() << "Skipping test; unavailable multi-screen support.";
     }
@@ -1350,6 +1355,7 @@
 #if BUILDFLAG(IS_MAC)
     ui::NSWindowFakedForTesting::SetEnabled(ns_window_faked_for_testing_);
 #endif
+    FullscreenControllerInteractiveTest::TearDownOnMainThread();
   }
 
   // Create virtual displays as needed, ensuring 2 displays are available for
@@ -1775,7 +1781,7 @@
 
   // Explicitly check for, and destroy, the exclusive access bubble.
   EXPECT_TRUE(IsExclusiveAccessBubbleDisplayed());
-  Wait(ExclusiveAccessBubble::kShowTime);
+  Wait(ExclusiveAccessBubble::kShowTime * 2);
   FinishExclusiveAccessBubbleAnimation();
   EXPECT_FALSE(IsExclusiveAccessBubbleDisplayed());
 
diff --git a/chrome/browser/ui/views/exclusive_access_bubble_views.cc b/chrome/browser/ui/views/exclusive_access_bubble_views.cc
index 5c0e580..db807b2e 100644
--- a/chrome/browser/ui/views/exclusive_access_bubble_views.cc
+++ b/chrome/browser/ui/views/exclusive_access_bubble_views.cc
@@ -24,10 +24,12 @@
 #include "chrome/grit/generated_resources.h"
 #include "components/fullscreen_control/fullscreen_features.h"
 #include "components/fullscreen_control/subtle_notification_view.h"
+#include "components/viz/common/frame_timing_details.h"
 #include "content/public/browser/web_contents.h"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
index 5b87012b..a129897 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
@@ -13,12 +13,13 @@
   explicit MockExclusiveAccessBubble(ExclusiveAccessBubbleParams params)
       : ExclusiveAccessBubble(params) {}
   ~MockExclusiveAccessBubble() override = default;
-  MOCK_METHOD(void, Hide, (), (override));
   MOCK_METHOD(void, Show, (), (override));
+  MOCK_METHOD(void, Hide, (), (override));
 
   using ExclusiveAccessBubble::hide_timeout_;
   using ExclusiveAccessBubble::ShowAndStartTimers;
   using ExclusiveAccessBubble::snooze_until_;
+  using ExclusiveAccessBubble::StartHideTimer;
 };
 
 class ExclusiveAccessBubbleTest : public testing::Test {
@@ -63,3 +64,20 @@
   task_environment_.FastForwardBy(base::Minutes(16));
   bubble_.OnUserInput();
 }
+
+TEST_F(ExclusiveAccessBubbleTest, StartHideTimerRestartsTimer) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  EXPECT_TRUE(bubble_.hide_timeout_.IsRunning());
+
+  task_environment_.FastForwardBy(base::Seconds(1));
+  auto remaining =
+      bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+  EXPECT_LT(remaining, ExclusiveAccessBubble::kShowTime);
+
+  // Verify that the time increases.
+  bubble_.StartHideTimer();
+  auto new_remaining =
+      bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
+  EXPECT_LT(remaining, new_remaining);
+}
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
index 2756a83..91fd829 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_test.cc
@@ -74,6 +74,8 @@
 ExclusiveAccessTest::~ExclusiveAccessTest() = default;
 
 void ExclusiveAccessTest::SetUpOnMainThread() {
+  ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(true);
+
   permission_controller_ =
       std::make_unique<content::MockPermissionController>();
   ON_CALL(*permission_controller_, RequestPermissionsFromCurrentDocument)
@@ -110,6 +112,8 @@
 }
 
 void ExclusiveAccessTest::TearDownOnMainThread() {
+  ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(false);
+
   GetExclusiveAccessManager()
       ->pointer_lock_controller()
       ->bubble_hide_callback_for_test_ =
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 e34c562..0245179 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -80,13 +80,13 @@
 }  // namespace
 
 class FullscreenControllerInteractiveTest : public ExclusiveAccessTest {
+ protected:
   void SetUpOnMainThread() override {
     ExclusiveAccessTest::SetUpOnMainThread();
 
     SetDisableFullscreenWithinTab(true);
   }
 
- protected:
   void SetUpCommandLine(base::CommandLine* command_line) override {
     ExclusiveAccessTest::SetUpCommandLine(command_line);
     // Slow bots are flaky due to slower loading interacting with
@@ -981,6 +981,7 @@
   }
 
   void SetUpOnMainThread() override {
+    FullscreenControllerInteractiveTest::SetUpOnMainThread();
     auto allow_automatic_fullscreen = [&](const GURL& url) {
       HostContentSettingsMapFactory::GetForProfile(browser()->profile())
           ->SetContentSettingDefaultScope(
@@ -1014,7 +1015,10 @@
     ASSERT_TRUE(WaitForRenderFrameReady(web_contents_->GetPrimaryMainFrame()));
   }
 
-  void TearDownOnMainThread() override { web_contents_ = nullptr; }
+  void TearDownOnMainThread() override {
+    web_contents_ = nullptr;
+    FullscreenControllerInteractiveTest::TearDownOnMainThread();
+  }
 
   bool RequestFullscreen(bool gesture = false,
                          content::RenderFrameHost* rfh = nullptr) {
@@ -1327,6 +1331,7 @@
     : public FullscreenControllerInteractiveTest {
  public:
   void SetUpOnMainThread() override {
+    FullscreenControllerInteractiveTest::SetUpOnMainThread();
     if (!SetUpVirtualDisplays()) {
       GTEST_SKIP() << "Skipping test; unavailable multi-screen support.";
     }
@@ -1350,6 +1355,7 @@
 #if BUILDFLAG(IS_MAC)
     ui::NSWindowFakedForTesting::SetEnabled(ns_window_faked_for_testing_);
 #endif
+    FullscreenControllerInteractiveTest::TearDownOnMainThread();
   }
 
   // Create virtual displays as needed, ensuring 2 displays are available for
@@ -1775,7 +1781,7 @@
 
   // Explicitly check for, and destroy, the exclusive access bubble.
   EXPECT_TRUE(IsExclusiveAccessBubbleDisplayed());
-  Wait(ExclusiveAccessBubble::kShowTime);
+  Wait(ExclusiveAccessBubble::kShowTime * 2);
   FinishExclusiveAccessBubbleAnimation();
   EXPECT_FALSE(IsExclusiveAccessBubbleDisplayed());
Loading diff…

Original Bug Report

reported by [email protected]

WebGL Fullscreen Security UI Bypass

VULNERABILITY DETAILS WebGL allows malicious actors to overload GPUs and stall rendering of all Chromium/OS UI elements. Utilizing this, it is possible to obscure the dialog making users aware the browser is entering fullscreen mode (see WHATWG Fullscreen API Living Standard: users should always be informed when something is displayed in fullscreen [1]).

After a user interaction (required to use the fullscreen API), the GPU rendering can be blocked. Meanwhile the browser enters fullscreen. This is not visible to users since any rendering is stalled by resource exhaustion. After rendering has recovered, the browser should be in fullscreen, with the fullscreen notification completely skipped.

Our testing shows that the exploit only works in Windows-based systems, Linux systems are unaffected, as we were not able to reproduce the described behavior in those. This indicates that some Windows-specific implementation may be the reason for this behavior.

The exploit can be utilized for example to create convincing Windows lock-screen look-alike phishing site (if you want a more fleshed-out PoC we can provide a video and/or source code for a full demonstration).

[1] https://fullscreen.spec.whatwg.org/#security-and-privacy-considerations

VERSION Chrome Version: 124.0.6367.208 stable Operating System: Windows 10 Pro, 22H2

REPRODUCTION CASE The attached file ‘minimal_chrome.html’ demonstrates the exploit. It may be required to adjust the STRESS, FULLSCREEN_WAIT and RENDERTIME values, depending on the device. The given values give consistent results when testing on a Lenovo Thinkpad P14s Gen 3 using the integrated Intel Iris Xe Graphics for rendering.

CREDIT INFORMATION Reporter credit: Wolfgang Ettlinger (aff. Certitude Consulting GmbH) Alexander Hurbean (aff. Certitude Consulting GmbH)

View on issue tracker