Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Browser
DescriptionUI misrepresentation in Browser
ComponentBrowser
Bug ClassLogic Error
Tracker514078852
Fix commitd124770dd6b6 (chromium/src) +63/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

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

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/views/exclusive_access/exclusive_access_bubble_views.cc
From d124770dd6b6b289f9bb01276e024e66d4811abc Mon Sep 17 00:00:00 2001
From: Dana Fried <[email protected]>
Date: Wed, 22 Jul 2026 17:05:17 -0700
Subject: [PATCH] [Exclusive Access Bubble] Fix issue with 'show on next interaction'

There were certain conditions which caused the "how to exit fullscreen"
bubble to show on the next user interaction without a snooze happening.
However, since this state was stored as a special snooze timer value,
another show attempt via e.g. downloads would put the bubble into an
incorrect state.

This separates the two along with encapsulating a lot of stuff in the
base class that should ideally be private.

It also adds some new tests to cover edge cases fixed by the change.

Fixed: 514078852
Change-Id: I8429831baa69478c409f11dee1fc58377a9194b0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8120956
Reviewed-by: David Pennington <[email protected]>
Commit-Queue: Dana Fried <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1666719}
---

diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
index 3bc3c10..a8a3c2c 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
@@ -19,7 +19,8 @@
 
 void ExclusiveAccessBubble::OnUserInput() {
   // Re-show the bubble if no user input occurred during the snooze period.
-  if (base::TimeTicks::Now() > snooze_until_) {
+  if (must_show_next_interaction_ || base::TimeTicks::Now() > snooze_until_) {
+    must_show_next_interaction_ = false;
     ShowAndStartTimers();
   }
 
@@ -40,3 +41,7 @@
   // Restart the snooze period; to only re-show after a period of inactivity.
   snooze_until_ = base::TimeTicks::Now() + kSnoozeTime;
 }
+
+void ExclusiveAccessBubble::SetMustShowOnNextInteraction() {
+  must_show_next_interaction_ = true;
+}
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
index 6325a826..155547f 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_UI_EXCLUSIVE_ACCESS_EXCLUSIVE_ACCESS_BUBBLE_H_
 #define CHROME_BROWSER_UI_EXCLUSIVE_ACCESS_EXCLUSIVE_ACCESS_BUBBLE_H_
 
+#include "base/gtest_prod_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/timer/timer.h"
 #include "chrome/browser/ui/exclusive_access/exclusive_access_bubble_type.h"
@@ -48,18 +49,28 @@
   // Reset the timeout for user input before we auto-show again.
   void Snooze();
 
+  // Indicate that the bubble must show immediately on next user interaction,
+  // regardless of timeouts.
+  void SetMustShowOnNextInteraction();
+
   // Cached content and traits for this bubble.
   ExclusiveAccessBubbleParams params_;
 
-  // Hides the bubble after it has been displayed for a short time.
-  base::RetainingOneShotTimer hide_timeout_;
-
-  // Bubble re-shows on user input are suppressed until this time elapses.
-  base::TimeTicks snooze_until_;
-
  private:
   friend class ExclusiveAccessTest;
   friend class ExclusiveAccessBubbleViewsTest;
+  FRIEND_TEST_ALL_PREFIXES(ExclusiveAccessBubbleTest, ShowAndStartTimers);
+  FRIEND_TEST_ALL_PREFIXES(ExclusiveAccessBubbleTest,
+                           StartHideTimerRestartsTimer);
+
+  // Bubble re-shows on user input are suppressed until this time elapses.
+  base::TimeTicks snooze_until_;
+
+  // Hides the bubble after it has been displayed for a short time.
+  base::RetainingOneShotTimer hide_timeout_;
+
+  // Flag indicating that the bubble must show on the next user interaction.
+  bool must_show_next_interaction_ = false;
 };
 
 #endif  // CHROME_BROWSER_UI_EXCLUSIVE_ACCESS_EXCLUSIVE_ACCESS_BUBBLE_H_
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 a129897..2fa4cd5 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
@@ -16,14 +16,16 @@
   MOCK_METHOD(void, Show, (), (override));
   MOCK_METHOD(void, Hide, (), (override));
 
-  using ExclusiveAccessBubble::hide_timeout_;
+  using ExclusiveAccessBubble::SetMustShowOnNextInteraction;
   using ExclusiveAccessBubble::ShowAndStartTimers;
-  using ExclusiveAccessBubble::snooze_until_;
   using ExclusiveAccessBubble::StartHideTimer;
 };
 
 class ExclusiveAccessBubbleTest : public testing::Test {
  public:
+  static constexpr base::TimeDelta kEnsureHideTime =
+      ExclusiveAccessBubble::kShowTime + base::Seconds(1);
+
   ExclusiveAccessBubbleTest()
       : bubble_(
             {.type =
@@ -81,3 +83,37 @@
       bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
   EXPECT_LT(remaining, new_remaining);
 }
+
+TEST_F(ExclusiveAccessBubbleTest, ReshowOnFirstInputPreservedByStartHideTimer) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  bubble_.SetMustShowOnNextInteraction();
+  bubble_.StartHideTimer();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.OnUserInput();
+}
+
+TEST_F(ExclusiveAccessBubbleTest,
+       ReshowOnFirstInputPreservedByShowAndStartTimers) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  bubble_.SetMustShowOnNextInteraction();
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.OnUserInput();
+}
+
+TEST_F(ExclusiveAccessBubbleTest, ReshowOnFirstInputOnlyOnce) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  bubble_.SetMustShowOnNextInteraction();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.OnUserInput();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(0);
+  bubble_.OnUserInput();
+}
diff --git a/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc b/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc
index 17f5d05..8e8a3ec 100644
--- a/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc
+++ b/chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.cc
@@ -143,7 +143,7 @@
                                   ->fullscreen_controller()
                                   ->exclusive_access_tab();
   if (entering_tab_fullscreen && tab && !tab->HasRecentInteraction()) {
-    snooze_until_ = base::TimeTicks::Min();
+    SetMustShowOnNextInteraction();
   }
 }
 
@@ -214,7 +214,7 @@
                                   ->fullscreen_controller()
                                   ->exclusive_access_tab();
   if (entering_tab_fullscreen && tab && !tab->HasRecentInteraction()) {
-    snooze_until_ = base::TimeTicks::Min();
+    SetMustShowOnNextInteraction();
   }
 }
 
@@ -397,7 +397,6 @@
   // This function is guarded by the `ExclusiveAccessBubble::hide_timeout_`
   // timer, so the bubble has been displayed for at least
   // `ExclusiveAccessBubble::kShowTime`.
-  DCHECK(!hide_timeout_.IsRunning());
   RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kTimeout);
   presentation_cb_.Reset();
 
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 a129897..2fa4cd5 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble_unittest.cc
@@ -16,14 +16,16 @@
   MOCK_METHOD(void, Show, (), (override));
   MOCK_METHOD(void, Hide, (), (override));
 
-  using ExclusiveAccessBubble::hide_timeout_;
+  using ExclusiveAccessBubble::SetMustShowOnNextInteraction;
   using ExclusiveAccessBubble::ShowAndStartTimers;
-  using ExclusiveAccessBubble::snooze_until_;
   using ExclusiveAccessBubble::StartHideTimer;
 };
 
 class ExclusiveAccessBubbleTest : public testing::Test {
  public:
+  static constexpr base::TimeDelta kEnsureHideTime =
+      ExclusiveAccessBubble::kShowTime + base::Seconds(1);
+
   ExclusiveAccessBubbleTest()
       : bubble_(
             {.type =
@@ -81,3 +83,37 @@
       bubble_.hide_timeout_.desired_run_time() - base::TimeTicks::Now();
   EXPECT_LT(remaining, new_remaining);
 }
+
+TEST_F(ExclusiveAccessBubbleTest, ReshowOnFirstInputPreservedByStartHideTimer) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  bubble_.SetMustShowOnNextInteraction();
+  bubble_.StartHideTimer();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.OnUserInput();
+}
+
+TEST_F(ExclusiveAccessBubbleTest,
+       ReshowOnFirstInputPreservedByShowAndStartTimers) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  bubble_.SetMustShowOnNextInteraction();
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.OnUserInput();
+}
+
+TEST_F(ExclusiveAccessBubbleTest, ReshowOnFirstInputOnlyOnce) {
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.ShowAndStartTimers();
+  bubble_.SetMustShowOnNextInteraction();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(1);
+  bubble_.OnUserInput();
+  task_environment_.FastForwardBy(kEnsureHideTime);
+  EXPECT_CALL(bubble_, Show()).Times(0);
+  bubble_.OnUserInput();
+}
Loading diff…

Original Bug Report

reported by [email protected]

Bypass of fullscreen exit instruction reminder via automatic download

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 error in ExclusiveAccessBubbleViews::Update() allows an automatic download notification to overwrite the ’re-show on first input’ security mitigation for gesture-less fullscreen. This allows a site to suppress the exit instruction reminder, facilitating persistent UI spoofing in fullscreen mode.

Affected files:

  • chrome/browser/ui/views/exclusive_access_bubble_views.cc
  • chrome/browser/ui/exclusive_access/exclusive_access_bubble.cc
  • chrome/browser/download/bubble/download_display_controller.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A logic error in ExclusiveAccessBubbleViews::Update() causes the security mitigation for gesture-less fullscreen entry to be clobbered by subsequent updates, such as those triggered by automatic downloads. This mitigation is designed to ensure that if a site enters fullscreen without a user gesture (e.g., via the Automatic Fullscreen permission), an exit instruction bubble is re-shown upon the first user interaction. By triggering a download immediately after entry, a malicious site can suppress this reminder.

Root Cause Analysis

When a tab enters fullscreen without recent user interaction, ExclusiveAccessBubbleViews sets a ‘sentinel’ value to ensure the exit instruction re-appears when the user eventually interacts with the page. This is handled in the constructor:

// chrome/browser/ui/views/exclusive_access_bubble_views.cc:134
if (entering_tab_fullscreen && tab && !tab->HasRecentInteraction()) {
  snooze_until_ = base::TimeTicks::Min();
}

The base::TimeTicks::Min() value tells ExclusiveAccessBubble::OnUserInput() to ignore the standard 15-minute ‘snooze’ period and show the bubble immediately.

However, if a download starts while the bubble is active or recently hidden, DownloadDisplayController::OnNewItem calls UpdateExclusiveAccessBubble with {.has_download=true, .force_update=true}. This triggers ExclusiveAccessBubbleViews::Update():

  1. Update() calls ShowAndStartTimers() (line 198), which calls Snooze().
  2. Snooze() unconditionally resets the snooze timer: snooze_until_ = base::TimeTicks::Now() + kSnoozeTime (15 minutes).
  3. Update() then evaluates entering_tab_fullscreen to determine if the sentinel should be restored (line 186): const bool entering_tab_fullscreen = !IsTabFullscreenType(params_.type) && IsTabFullscreenType(params.type);
  4. Since the tab is already in fullscreen, IsTabFullscreenType(params_.type) is already true, so entering_tab_fullscreen evaluates to false.
  5. The code skips the block that restores the base::TimeTicks::Min() sentinel (lines 205-207).

Consequently, the 15-minute snooze period remains in effect, and the exit instruction will not be re-shown when the user interacts with the page.

Potential Attack Scenario

An attacker could follow these suggested steps to trigger the vulnerability (note that our tooling cannot currently execute code to verify this):

  1. A user visits a site with Automatic Fullscreen permission (e.g., an Isolated Web App or an enterprise-managed site).
  2. The site waits for several seconds of inactivity to ensure HasRecentInteraction() is false.
  3. The site programmatically enters fullscreen.
  4. The site immediately initiates an automatic download (permitted for the first download in a tab without a gesture).
  5. The download notification clobbers the snooze_until_ sentinel in the browser process.
  6. The user, unaware that the site entered fullscreen automatically, interacts with the page. The security instruction that should have appeared is suppressed, allowing the site to spoof browser UI elements effectively.

Suggested Fix

Modify ExclusiveAccessBubbleViews::Update() to preserve or re-evaluate the snooze_until_ sentinel if the bubble is already in a tab-initiated fullscreen state and the tab still lacks recent interaction. Specifically, the logic should ensure that a ‘forced update’ from a download does not discard the security state intended to protect gesture-less entries.

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.

View on issue tracker