CVE-2026-11216
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/file_select_helper.cc |
modified | |
WebContentschrome/browser/file_select_helper.h |
modified | |
TabInterfacechrome/browser/file_select_helper.h |
modified | |
DialogModelchrome/browser/file_select_helper.h |
modified | |
ASSERT_TRUEchrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc |
modified | |
FileSystemAccessBrowserTestForWebUIchrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc |
modified |
Files Changed
chrome/browser/file_select_helper.ccchrome/browser/file_select_helper.hchrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
Patch
From 9c6ca0273599b9e2713247bb751a1891ad05adf4 Mon Sep 17 00:00:00 2001
From: Alison Gale <agale@chromium.org>
Date: Thu, 23 Apr 2026 16:45:57 -0700
Subject: [PATCH] [SxS] Cancel file picker if active tab changes
This handles the case where a tab gets deactivated while the file picker
is open. One example of this happening is in split view where the user
is mid drag and drop when the file picker is opened.
#top-chrome-bug-fixit
Bug: 454484864,474583539
Change-Id: I91e869ea26b0884c5beb358516c602ec8178d5c9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7154160
Commit-Queue: Alison Gale <agale@chromium.org>
Reviewed-by: Dana Fried <dfried@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619839}
---
diff --git a/chrome/browser/file_select_helper.cc b/chrome/browser/file_select_helper.cc
index eb9e243..b8a24fda 100644
--- a/chrome/browser/file_select_helper.cc
+++ b/chrome/browser/file_select_helper.cc
@@ -30,6 +30,7 @@
#include "chrome/grit/generated_resources.h"
#include "components/enterprise/buildflags/buildflags.h"
#include "components/enterprise/common/proto/connectors.pb.h"
+#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/file_select_listener.h"
@@ -557,6 +558,14 @@
listener_ = std::move(listener);
content::WebContentsObserver::Observe(web_contents_);
+ tabs::TabInterface* tab_interface =
+ tabs::TabInterface::MaybeGetFromContents(web_contents_);
+ if (tab_interface) {
+ tab_deactivated_subscription_ =
+ tab_interface->RegisterWillDeactivate(base::BindRepeating(
+ &FileSelectHelper::OnTabDeactivated, base::Unretained(this)));
+ }
+
#if !BUILDFLAG(IS_ANDROID)
if (PictureInPictureWindowManager::GetInstance()
->ShouldFileDialogBlockPictureInPicture(web_contents_)) {
@@ -676,6 +685,8 @@
scoped_tuck_picture_in_picture_.reset();
#endif // !BUILDFLAG(IS_ANDROID)
+ tab_deactivated_subscription_ = {};
+
// If there are temporary files, then this instance needs to stick around
// until web_contents_ is destroyed, so that this instance can delete the
// temporary files.
@@ -755,6 +766,10 @@
CleanUp();
}
+void FileSelectHelper::OnTabDeactivated(tabs::TabInterface* tab) {
+ RunFileChooserEnd();
+}
+
// static
bool FileSelectHelper::IsAcceptTypeValid(const std::string& accept_type) {
// TODO(raymes): This only does some basic checks, extend to test more cases.
diff --git a/chrome/browser/file_select_helper.h b/chrome/browser/file_select_helper.h
index 58b1bfe9..37c575db 100644
--- a/chrome/browser/file_select_helper.h
+++ b/chrome/browser/file_select_helper.h
@@ -8,6 +8,7 @@
#include <memory>
#include <vector>
+#include "base/callback_list.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
@@ -34,6 +35,10 @@
class WebContents;
}
+namespace tabs {
+class TabInterface;
+}
+
namespace ui {
class DialogModel;
struct SelectedFileInfo;
@@ -143,6 +148,8 @@
void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
void WebContentsDestroyed() override;
+ void OnTabDeactivated(tabs::TabInterface* tab);
+
void EnumerateDirectoryImpl(
content::WebContents* tab,
scoped_refptr<content::FileSelectListener> listener,
@@ -320,6 +327,8 @@
// Set to false in unit tests since there is no WebContents.
bool abort_on_missing_web_contents_in_tests_ = true;
+ base::CallbackListSubscription tab_deactivated_subscription_;
+
#if !BUILDFLAG(IS_ANDROID)
// When not null, this prevents picture-in-picture windows from opening.
std::unique_ptr<ScopedDisallowPictureInPicture>
diff --git a/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc b/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
index 98be5531..2594a266 100644
--- a/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
+++ b/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
@@ -26,6 +26,8 @@
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_actions.h"
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/file_system_access/file_system_access_test_utils.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
@@ -1356,6 +1358,42 @@
ASSERT_EQ("AbortError", content::EvalJs(first_tab, "window.p"));
}
+// Test that creating a split view while the dialog is showing closes the
+// dialog. https://crbug.com/474583539 https://crbug.com/454484864
+IN_PROC_BROWSER_TEST_P(FileSystemAccessBrowserTest,
+ ShowOpenFileThenHideDueToSplitView) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), embedded_test_server()->GetURL("/title1.html")));
+ content::WebContents* first_tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Open the dialog and wait until it's created.
+ content::SelectFileDialogRecorder recorder;
+ ui::SelectFileDialog::SetFactory(
+ std::make_unique<content::ObservableSelectFileDialogFactory>(
+ recorder.GetWeakPtr()));
+ ASSERT_EQ(42,
+ content::EvalJs(
+ first_tab,
+ "window.p = self.showOpenFilePicker().catch(e => e.name); 42"));
+ ASSERT_TRUE(base::test::RunUntil([&recorder]() {
+ return recorder.state != content::SelectFileDialogRecorder::kNotCreated;
+ }));
+
+ // Create a split view.
+ const int active_index = browser()->tab_strip_model()->active_index();
+ chrome::NewSplitTab(browser(),
+ split_tabs::SplitTabCreatedSource::kToolbarButton);
+ EXPECT_TRUE(content::WaitForLoadStop(
+ browser()->tab_strip_model()->GetWebContentsAt(active_index + 1)));
+
+ // The first tab should not be the active tab anymore.
+ ASSERT_NE(first_tab, browser()->tab_strip_model()->GetActiveWebContents());
+
+ // Check that the dialog was closed.
+ ASSERT_EQ("AbortError", content::EvalJs(first_tab, "window.p"));
+}
+
class FileSystemAccessBrowserTestForWebUI
: public InProcessBrowserTest,
public ::testing::WithParamInterface<bool> {
Regression Test / PoC
diff --git a/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc b/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
index 98be5531..2594a266 100644
--- a/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
+++ b/chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
@@ -26,6 +26,8 @@
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_actions.h"
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/file_system_access/file_system_access_test_utils.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
@@ -1356,6 +1358,42 @@
ASSERT_EQ("AbortError", content::EvalJs(first_tab, "window.p"));
}
+// Test that creating a split view while the dialog is showing closes the
+// dialog. https://crbug.com/474583539 https://crbug.com/454484864
+IN_PROC_BROWSER_TEST_P(FileSystemAccessBrowserTest,
+ ShowOpenFileThenHideDueToSplitView) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), embedded_test_server()->GetURL("/title1.html")));
+ content::WebContents* first_tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Open the dialog and wait until it's created.
+ content::SelectFileDialogRecorder recorder;
+ ui::SelectFileDialog::SetFactory(
+ std::make_unique<content::ObservableSelectFileDialogFactory>(
+ recorder.GetWeakPtr()));
+ ASSERT_EQ(42,
+ content::EvalJs(
+ first_tab,
+ "window.p = self.showOpenFilePicker().catch(e => e.name); 42"));
+ ASSERT_TRUE(base::test::RunUntil([&recorder]() {
+ return recorder.state != content::SelectFileDialogRecorder::kNotCreated;
+ }));
+
+ // Create a split view.
+ const int active_index = browser()->tab_strip_model()->active_index();
+ chrome::NewSplitTab(browser(),
+ split_tabs::SplitTabCreatedSource::kToolbarButton);
+ EXPECT_TRUE(content::WaitForLoadStop(
+ browser()->tab_strip_model()->GetWebContentsAt(active_index + 1)));
+
+ // The first tab should not be the active tab anymore.
+ ASSERT_NE(first_tab, browser()->tab_strip_model()->GetActiveWebContents());
+
+ // Check that the dialog was closed.
+ ASSERT_EQ("AbortError", content::EvalJs(first_tab, "window.p"));
+}
+
class FileSystemAccessBrowserTestForWebUI
: public InProcessBrowserTest,
public ::testing::WithParamInterface<bool> {
Original Bug Report
Regression 440523110: User Activation Bypass via showOpenFilePicker and contextmenu delay
Steps to reproduce the problem
- Open the attached poc-spooff.html
- Right-click the link and select “Open in Split View”.
- Observe the right pane displaying drive.google.com (trusted origin).
- After ~1 second, a native file picker dialog appears.
- Select an image file.
- The selected image is rendered fully in the left (attacker-controlled) split pane, despite the user being visually anchored to Google Drive.
Problem Description
This is a regression bypass of issue 440523110, not a new variant. The previous fix correctly invalidates user activation for input[type=file].click() when triggered asynchronously after contextmenu. However, the same user-activation invalidation does not apply to showOpenFilePicker().
As a result:
- contextmenu is still treated as a valid user activation source.
- User activation persists across setTimeout when calling File System Access API.
- Opening a link in Split View does not invalidate or reset the activation state.
- The file picker can be invoked by an attacker-controlled origin, while the user is visually anchored to a trusted origin (drive.google.com).
This creates inconsistent enforcement between legacy file input APIs and the modern File System Access API, effectively bypassing the intended fix in 440523110.
The issue is not permission bypass, but trust misattribution / UX spoofing: users reasonably believe the file picker originates from Google Drive, while it is actually triggered by the attacker page.
Additional Comments
For additional validation, the same proof-of-concept was reproduced on Brave Browser (latest stable version) under identical conditions. In Brave, performing the same contextmenu + Split View interaction does not trigger any file picker or file explorer UI at all. The file selection dialog is fully blocked.
This demonstrates that the issue is not an inherent or required web platform behavior, but rather a Chromium-specific regression in how user activation is preserved for showOpenFilePicker() after asynchronous execution.
A short demonstration video recorded on Brave is attached as braveExpectedResults.mp4, showing the expected behavior where no file picker is displayed and no trust confusion occurs.
This comparison further strengthens the conclusion that Chromium currently enforces user activation inconsistently between legacy file inputs and the File System Access API, and that stricter invalidation (as implemented by Brave) is feasible and effective.
Summary
Regression 440523110: User Activation Bypass via showOpenFilePicker and contextmenu delay
Custom Questions
Type of crash:
N/A
Crash state:
N/A
Reporter credit:
Azza Tegar Naufal Ataullah
Additional Data
Category: Security
Chrome Channel: Stable
Regression: Yes \