CVE-2026-13984
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.cc |
modified | |
BindLambdaForTestingchrome/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.ccchrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.hchrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view_browsertest.cc
Patch
From 3a87dcef44cd5383581bb7e88bb9377882cd3eb1 Mon Sep 17 00:00:00 2001
From: Alison Gale <agale@chromium.org>
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 <agale@chromium.org>
Reviewed-by: Muhammad Salmaan <musalmaan@chromium.org>
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));
Regression Test / PoC
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));
Original Bug Report
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.ccchrome/browser/ui/views/frame/multi_contents_view.ccchrome/browser/ui/views/file_system_access/file_system_access_restore_permission_bubble_view.ccchrome/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
-
Highlight Misattribution: The
SplitTabHighlightController(located inchrome/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. TheShouldHighlight()method returns true if any tracked bubble, such as the FSA restore bubble (kFileSystemAccessBubbleElementIdentifier), is visible in the window. However,UpdateHighlight()callsSetHighlightActiveContentsView(true)on its delegate, which is theMultiContentsView. -
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. -
Bubble Persistence: The
FileSystemAccessRestorePermissionBubbleViewexplicitly disablesclose_on_deactivate(set_close_on_deactivate(false)inchrome/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. -
Anchoring and Omnibox: In Split View, the window’s main toolbar and omnibox reflect the state of the active pane. Because
FileSystemAccessRestorePermissionBubbleViewis aLocationBarBubbleDelegateView, 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. -
Lack of Activation Check:
FileSystemAccessPermissionRequestManagerdoes not verify if its associatedWebContentsis 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
- Enable the Split View (Side-by-Side) feature (e.g., via
chrome://flags). - Open a Split View tab with
https://victim.examplein Pane A andhttps://attacker.examplein Pane B. - Ensure
attacker.examplehas a dormant/persisted File System Access grant (e.g., a handle saved to IndexedDB). - Make Pane A the active pane (click it; the omnibox shows
victim.example). - From the background (Pane B), trigger the FSA restore prompt (e.g., by calling
handle.requestPermission()). - Observe that the bubble anchors to the
victim.exampletoolbar and a highlight ring is drawn around Pane A, despite Pane B being the requester.
Suggested Fix
- Active Tab Check: Modify
FileSystemAccessPermissionRequestManager::CanShowRequestto ensure the requestingWebContentsis the active pane in the split view before proceeding with the bubble. - Context-Aware Highlighting: Update
SplitTabHighlightControllerto identify the specificWebContentsor pane associated with the visible bubble and update theMultiContentsViewdelegate 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.