Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in TabStrip
DescriptionIncorrect security UI in TabStrip
ComponentTabStrip
Bug ClassLogic Error
Tracker514010404
Fix commit3a87dcef44cd (chromium/src) +56/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc
modified
BindLambdaForTesting
chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc
  • chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.h
  • chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
From 3a87dcef44cd5383581bb7e88bb9377882cd3eb1 Mon Sep 17 00:00:00 2001
From: Alison Gale <[email protected]>
Date: Fri, 22 May 2026 10:45:03 -0700
Subject: [PATCH] [SxS] Hide FSA bubble when tab is inactive

This bubble hides when you switch tabs, but if you are in split view and
the bubble is triggered for the inactive view, it appears anchored to
the omnibox but it is for the wrong site. This change blocks the bubble
if the triggering tab is inactive.

Change-Id: I688b4ec4bbeea804e6e45e2f42052c9e31c23ff6
Bug: 514010404
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7870349
Commit-Queue: Alison Gale <[email protected]>
Reviewed-by: Muhammad Salmaan <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1635073}
---

diff --git a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc
index 71ff4fd97..efede78 100644
--- a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc
+++ b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc
@@ -14,6 +14,7 @@
 #include "chrome/grit/generated_resources.h"
 #include "components/permissions/permission_util.h"
 #include "components/strings/grit/components_strings.h"
+#include "components/tabs/public/tab_interface.h"
 #include "ui/base/interaction/element_identifier.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
@@ -38,6 +39,17 @@
     : LocationBarBubbleDelegateView(anchor, web_contents),
       window_title_(window_title),
       callback_(std::move(callback)) {
+  if (web_contents) {
+    if (auto* tab_interface =
+            tabs::TabInterface::MaybeGetFromContents(web_contents)) {
+      tab_deactivation_subscription_ =
+          tab_interface->RegisterWillDeactivate(base::BindRepeating(
+              [](FileSystemAccessRestorePermissionBubbleView* bubble_view,
+                 tabs::TabInterface* tab) { bubble_view->CloseBubble(); },
+              base::Unretained(this)));
+    }
+  }
+
   // Initial set up.
   views::LayoutProvider* layout_provider = views::LayoutProvider::Get();
   SetLayoutManager(std::make_unique<views::BoxLayout>(
@@ -126,7 +138,8 @@
 
   auto* browser =
       GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents);
-  if (!browser || !browser->GetWindow()) {
+  if (!browser || !browser->GetWindow() || !browser->GetActiveTabInterface() ||
+      browser->GetActiveTabInterface()->GetContents() != web_contents) {
     return nullptr;
   }
 
@@ -149,6 +162,11 @@
   return bubble_view;
 }
 
+void FileSystemAccessRestorePermissionBubbleView::CloseBubble() {
+  tab_deactivation_subscription_ = {};
+  LocationBarBubbleDelegateView::CloseBubble();
+}
+
 void FileSystemAccessRestorePermissionBubbleView::AddedToWidget() {
   GetBubbleFrameView()->SetTitleView(CreateTitleOriginLabel(GetWindowTitle()));
 }
diff --git a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.h b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.h
index a889673..72c98b3 100644
--- a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.h
+++ b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_UI_VIEWS_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_RESTORE_PERMISSION_BUBBLE_VIEW_H_
 #define CHROME_BROWSER_UI_VIEWS_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_RESTORE_PERMISSION_BUBBLE_VIEW_H_
 
+#include "base/callback_list.h"
 #include "chrome/browser/file_system_access/file_system_access_permission_request_manager.h"
 #include "chrome/browser/ui/views/location_bar/location_bar_bubble_delegate_view.h"
 #include "components/permissions/permission_util.h"
@@ -49,6 +50,9 @@
       base::OnceCallback<void(permissions::PermissionAction)> callback,
       content::WebContents* web_contents);
 
+  // LocationBarBubbleDelegateView:
+  void CloseBubble() override;
+
   // views::BubbleDialogDelegateView:
   void AddedToWidget() override;
   bool ShouldShowCloseButton() const override;
@@ -67,6 +71,7 @@
  private:
   const std::u16string window_title_;
   base::OnceCallback<void(permissions::PermissionAction)> callback_;
+  base::CallbackListSubscription tab_deactivation_subscription_;
 };
 
 void ShowFileSystemAccessRestorePermissionDialog(
diff --git a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
index 644b001..8bfe9bf 100644
--- a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
+++ b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
@@ -130,6 +130,38 @@
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemAccessRestorePermissionBubbleViewTest,
+                       BubbleDismissedOnTabSwitch) {
+  ASSERT_TRUE(AddTabAtIndex(1, GetURL("foo.com"),
+                            ui::PageTransition::PAGE_TRANSITION_TYPED));
+  browser()->tab_strip_model()->ActivateTabAt(0);
+
+  permissions::PermissionAction callback_result;
+  GetFileSystemAccessRestorePermissionDialogForTesting(
+      kRequestData,
+      base::BindLambdaForTesting([&](permissions::PermissionAction result) {
+        callback_result = result;
+      }),
+      browser()->tab_strip_model()->GetWebContentsAt(0));
+
+  browser()->tab_strip_model()->ActivateTabAt(1);
+
+  EXPECT_EQ(callback_result, permissions::PermissionAction::DISMISSED);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemAccessRestorePermissionBubbleViewTest,
+                       NotCreatedForInactiveTab) {
+  ASSERT_TRUE(AddTabAtIndex(1, GetURL("foo.com"),
+                            ui::PageTransition::PAGE_TRANSITION_TYPED));
+  browser()->tab_strip_model()->ActivateTabAt(1);
+
+  auto* bubble = GetFileSystemAccessRestorePermissionDialogForTesting(
+      kRequestData, base::DoNothing(),
+      browser()->tab_strip_model()->GetWebContentsAt(0));
+
+  EXPECT_EQ(bubble, nullptr);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemAccessRestorePermissionBubbleViewTest,
                        ShowFileSystemAccessDialog) {
   ASSERT_TRUE(AddTabAtIndex(0, GetURL("example.com"),
                             ui::PageTransition::PAGE_TRANSITION_TYPED));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
index 644b001..8bfe9bf 100644
--- a/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
+++ b/chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
@@ -130,6 +130,38 @@
 }
 
 IN_PROC_BROWSER_TEST_F(FileSystemAccessRestorePermissionBubbleViewTest,
+                       BubbleDismissedOnTabSwitch) {
+  ASSERT_TRUE(AddTabAtIndex(1, GetURL("foo.com"),
+                            ui::PageTransition::PAGE_TRANSITION_TYPED));
+  browser()->tab_strip_model()->ActivateTabAt(0);
+
+  permissions::PermissionAction callback_result;
+  GetFileSystemAccessRestorePermissionDialogForTesting(
+      kRequestData,
+      base::BindLambdaForTesting([&](permissions::PermissionAction result) {
+        callback_result = result;
+      }),
+      browser()->tab_strip_model()->GetWebContentsAt(0));
+
+  browser()->tab_strip_model()->ActivateTabAt(1);
+
+  EXPECT_EQ(callback_result, permissions::PermissionAction::DISMISSED);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemAccessRestorePermissionBubbleViewTest,
+                       NotCreatedForInactiveTab) {
+  ASSERT_TRUE(AddTabAtIndex(1, GetURL("foo.com"),
+                            ui::PageTransition::PAGE_TRANSITION_TYPED));
+  browser()->tab_strip_model()->ActivateTabAt(1);
+
+  auto* bubble = GetFileSystemAccessRestorePermissionDialogForTesting(
+      kRequestData, base::DoNothing(),
+      browser()->tab_strip_model()->GetWebContentsAt(0));
+
+  EXPECT_EQ(bubble, nullptr);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemAccessRestorePermissionBubbleViewTest,
                        ShowFileSystemAccessDialog) {
   ASSERT_TRUE(AddTabAtIndex(0, GetURL("example.com"),
                             ui::PageTransition::PAGE_TRANSITION_TYPED));
Loading diff…

Original Bug Report

reported by [email protected]

Potential UI misattribution of File System Access restore bubble in Split View mode

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: In Chrome’s split-view mode, the visual highlight ring and bubble anchoring for File System Access (FSA) restore prompts can be misattributed to the origin in the active pane. This occurs because the split-view highlight logic defaults to the active pane regardless of which WebContents initiated the request.

Affected files:

  • chrome/browser/ui/tabs/split_tab_highlight_controller.cc
  • chrome/browser/ui/views/frame/multi_contents_view.cc
  • chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc
  • chrome/browser/file_system_access/file_system_access_permission_request_manager.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Potential Vulnerability in Split View UI Attribution

A logic flaw has been identified in Chrome’s Split View (Side-by-Side) mode where visual cues for permission prompts—specifically the File System Access (FSA) restore bubble—are incorrectly attributed to the active pane’s origin rather than the requesting pane’s origin.

Technical Analysis

  1. Highlight Misattribution: The SplitTabHighlightController (located in chrome/browser/ui/tabs/split_tab_highlight_controller.cc) is responsible for triggering a highlight ring around a pane when a permission bubble or similar UI is shown. The ShouldHighlight() method returns true if any tracked bubble, such as the FSA restore bubble (kFileSystemAccessBubbleElementIdentifier), is visible in the window. However, UpdateHighlight() calls SetHighlightActiveContentsView(true) on its delegate, which is the MultiContentsView.

  2. Pane Highlighting: In MultiContentsView::UpdateContentsBorderAndOverlay (chrome/browser/ui/views/frame/multi_contents_view.cc), the logic always highlights the currently active pane (GetActiveContentsView()). If a background pane triggers a bubble while another pane is active, the highlight ring will surround the inactive (victim) pane instead of the requester.

  3. Bubble Persistence: The FileSystemAccessRestorePermissionBubbleView explicitly disables close_on_deactivate (set_close_on_deactivate(false) in chrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc). This allows the bubble to persist when the user switches focus between split panes, but the highlight ring will move to follow the new active pane.

  4. Anchoring and Omnibox: In Split View, the window’s main toolbar and omnibox reflect the state of the active pane. Because FileSystemAccessRestorePermissionBubbleView is a LocationBarBubbleDelegateView, it anchors to the toolbar’s Page Info icon. Consequently, the bubble appears anchored to the victim origin’s URL while the highlight ring surrounds the victim’s content pane.

  5. Lack of Activation Check: FileSystemAccessPermissionRequestManager does not verify if its associated WebContents is the currently active/visible pane before showing the restore prompt. This allows a background pane to silently trigger a bubble that appears associated with the foreground pane.

Potential Impact

An attacker-controlled origin in a background split-view pane can trigger an FSA restore prompt. The resulting UI incorrectly attributes the request to the origin in the active pane via the highlight ring and the anchored omnibox URL. This creates a high-fidelity spoofing scenario where a user might grant persistent file system access to the attacker, believing the request came from the site they are currently interacting with.

Suggested Potential Steps to Reproduce

  1. Enable the Split View (Side-by-Side) feature (e.g., via chrome://flags).
  2. Open a Split View tab with https://victim.example in Pane A and https://attacker.example in Pane B.
  3. Ensure attacker.example has a dormant/persisted File System Access grant (e.g., a handle saved to IndexedDB).
  4. Make Pane A the active pane (click it; the omnibox shows victim.example).
  5. From the background (Pane B), trigger the FSA restore prompt (e.g., by calling handle.requestPermission()).
  6. Observe that the bubble anchors to the victim.example toolbar and a highlight ring is drawn around Pane A, despite Pane B being the requester.

Suggested Fix

  1. Active Tab Check: Modify FileSystemAccessPermissionRequestManager::CanShowRequest to ensure the requesting WebContents is the active pane in the split view before proceeding with the bubble.
  2. Context-Aware Highlighting: Update SplitTabHighlightController to identify the specific WebContents or pane associated with the visible bubble and update the MultiContentsView delegate to highlight only that specific pane, rather than defaulting to the currently active one.

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