CVE-2026-0907
Overview
Background
- Split View (side-by-side, "SxS")
- a desktop Chrome mode where two tabs are displayed at once in one window, so a tab’s
WebContentscan be visible while it is not the active tab. - `WebContentsBasedCanceller`
- a
content/helper (aWebContentsObserver) that decides whether a browser-shown dialog such as the File System Access file chooser may be shown, and cancels it when itsCancelConditionbecomes true. - `CancelCondition::kVisibility`
- the cancel condition used for modal system dialogs like a file chooser; before the patch it meant only “cancel if
WebContents::GetVisibility()isHIDDEN” (plus an inactiveRenderFrameHost). - `WebContentsDelegate::IsContentsActive`
- the embedder hook that reports whether a given
WebContentsis the active tab in the browser.
Root Cause Analysis
Before the patch, the canceller’s show check returned CanShowForVisibility(web_contents()->GetVisibility()) && CanShowForRFHActiveState(), i.e. a file chooser was allowed whenever the requesting WebContents was not HIDDEN and its frame was active. That equated “visible” with “the tab the user is interacting with”, an assumption Split View breaks: a tab shown in the non-active pane is visible yet not active, so a page there could pop a file picker that appears over the window while the user’s attention and the browser’s active-tab UI belong to the other pane.
The patch adds CanShowForTabState() and ANDs it into the show check, tightening the condition: for CancelCondition::kVisibility with a non-null delegate, the dialog is only allowed when web_contents_delegate->IsContentsActive(web_contents()) is true. The new check is gated by features::kSideBySideFilePickerCancelling (enabled by default, described as a kill switch), and returns true (allow) when the feature is off, the condition is not kVisibility, or no delegate is set. The new browser test DontShowWhileInactive confirms showOpenFilePicker() now rejects with AbortError and no SelectFileDialog is created when the delegate reports the contents inactive. The commit message explicitly states this is partial: it only covers a tab that is inactive at request time, and does not yet observe a tab becoming inactive after the picker is shown.
WebContents visibility as a proxy for “this is the tab the user is looking at”, which is false in Split View; the fix adds an explicit active-tab check (IsContentsActive) to the kVisibility show condition.Attack Path
- Split View setup The victim has the attacker’s page open in one pane of a Split View, alongside another (trusted) site in the active pane.
- Picker requested from inactive pane
Script in the attacker’s visible-but-inactive tab calls a File System Access picker such as
window.showOpenFilePicker(). - Check passes pre-patch
Because the tab’s visibility is not
HIDDENand its frame is active,CanShowForVisibilityandCanShowForRFHActiveStateboth allow it, and the native file dialog is shown. - Misattributed dialog The user, focused on the active pane, may believe the picker belongs to the trusted site and select a file or directory, granting it to the attacker’s origin (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
InactiveWebContentsDelegatecontent/browser/file_system_access/file_system_chooser_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fcontent/browser/file_system_access/file_system_chooser_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cccontent/browser/file_system_access/file_system_chooser_browsertest.cccontent/browser/web_contents_based_canceller.cccontent/browser/web_contents_based_canceller.hcontent/common/features.cccontent/common/features.h
Audit Directions
- Visibility used as a proxy for focusSearch for other permission prompts, choosers, and modal dialogs that gate on
WebContents::GetVisibility()orOnVisibilityChangedalone and may misbehave when two tabs are simultaneously visible in Split View. - Request-time-only checksAs the commit message notes, the check runs when the dialog is requested; audit whether tab activation changes (pane switch, tab drag) after a dialog or prompt is shown are observed and cause cancellation.
- Null-delegate fail-open paths
CanShowForTabStateallows the dialog whenGetDelegate()returns null; review embedders and contexts (e.g. tabs being moved between windows) where the delegate may be absent or transient.
Patch
From c72906f92191aa760ae462ad5d879ab95ebbc472 Mon Sep 17 00:00:00 2001
From: Alison Gale <agale@chromium.org>
Date: Thu, 13 Nov 2025 13:19:00 -0800
Subject: [PATCH] [SxS] Block opening file picker from inactive tab
This only handles the first bug which is when the tab is inactive when
the file request happens. Will need to follow up to observe tab
activation changes to update when a tab becomes inactive.
Bug: 444653104
Change-Id: Ica1845ddaab9820d2bf1ecc419578042f2fce6f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7087491
Commit-Queue: Alison Gale <agale@chromium.org>
Reviewed-by: Bo Liu <boliu@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1544520}
---
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 3b618e1..078c7d2 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
@@ -716,7 +716,9 @@
" })})();"));
// Top-level page in first window picks files and sends it to iframe.
- first_party_web_contents->WasShown();
+ browser()->tab_strip_model()->ActivateTabAt(
+ browser()->tab_strip_model()->GetIndexOfWebContents(
+ first_party_web_contents));
EXPECT_EQ(test_file.BaseName().AsUTF8Unsafe(),
content::EvalJs(first_party_web_contents,
"(async () => {"
@@ -882,7 +884,9 @@
" })})();"));
// Top-level page in first window picks files and sends it to iframe.
- first_party_web_contents->WasShown();
+ browser()->tab_strip_model()->ActivateTabAt(
+ browser()->tab_strip_model()->GetIndexOfWebContents(
+ first_party_web_contents));
EXPECT_EQ(test_file.BaseName().AsUTF8Unsafe(),
content::EvalJs(first_party_web_contents,
"(async () => {"
diff --git a/content/browser/file_system_access/file_system_chooser_browsertest.cc b/content/browser/file_system_access/file_system_chooser_browsertest.cc
index 7aa930a..2bddf3e 100644
--- a/content/browser/file_system_access/file_system_chooser_browsertest.cc
+++ b/content/browser/file_system_access/file_system_chooser_browsertest.cc
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <memory>
#include <string>
#include <vector>
@@ -25,6 +26,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
@@ -2197,6 +2199,34 @@
EXPECT_EQ(recorder.state, SelectFileDialogRecorder::kNotCreated);
}
+class InactiveWebContentsDelegate : public content::WebContentsDelegate {
+ bool IsContentsActive(WebContents* wc) override { return false; }
+};
+
+IN_PROC_BROWSER_TEST_F(FileSystemChooserBrowserTest, DontShowWhileInactive) {
+ GURL url = embedded_test_server()->GetURL("/title1.html");
+ ASSERT_TRUE(NavigateToURL(shell(), url));
+
+ // Record the state of the dialog.
+ SelectFileDialogRecorder recorder;
+ ui::SelectFileDialog::SetFactory(
+ std::make_unique<ObservableSelectFileDialogFactory>(
+ recorder.GetWeakPtr()));
+
+ // Provide a WebContentsDelegate that marks all web contents as inactive.
+ WebContents* wc = shell()->web_contents();
+ std::unique_ptr<InactiveWebContentsDelegate> mock =
+ std::make_unique<InactiveWebContentsDelegate>();
+ wc->SetDelegate(mock.get());
+
+ // JS should see the dialog as aborted.
+ EXPECT_EQ(
+ "AbortError",
+ content::EvalJs(wc, "window.showOpenFilePicker().catch(e => e.name)"));
+ // The dialog should not have been created.
+ EXPECT_EQ(recorder.state, SelectFileDialogRecorder::kNotCreated);
+}
+
// TODO(crbug.com/457495639): We need a different way to detect when a
// WebContents is no longer displayed to the user for android since the
// intent to select a file always causes a HIDDEN event as the whole app
diff --git a/content/browser/web_contents_based_canceller.cc b/content/browser/web_contents_based_canceller.cc
index 6c2a39c..b72e3f36 100644
--- a/content/browser/web_contents_based_canceller.cc
+++ b/content/browser/web_contents_based_canceller.cc
@@ -4,9 +4,13 @@
#include "content/browser/web_contents_based_canceller.h"
+#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
+#include "content/common/features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_delegate.h"
+
namespace content {
// static
@@ -37,7 +41,7 @@
return false;
}
return CanShowForVisibility(web_contents()->GetVisibility()) &&
- CanShowForRFHActiveState();
+ CanShowForRFHActiveState() && CanShowForTabState();
}
bool WebContentsBasedCanceller::CanShowForVisibility(Visibility visibility) {
@@ -50,6 +54,21 @@
return render_frame_host && render_frame_host->IsActive();
}
+bool WebContentsBasedCanceller::CanShowForTabState() {
+ if (!base::FeatureList::IsEnabled(
+ features::kSideBySideFilePickerCancelling)) {
+ return true;
+ }
+ // Within Split View, it is possible for the tab containing a WebContents to
+ // be visible but not active. This scenario is considered a cancel condition
+ // for kVisibility rather than kActiveState because kActiveState is determined
+ // by the RenderFrameHost state, while kVisibility is determined by the
+ // WebContents state.
+ WebContentsDelegate* web_contents_delegate = web_contents()->GetDelegate();
+ return condition_ != CancelCondition::kVisibility || !web_contents_delegate ||
+ web_contents_delegate->IsContentsActive(web_contents());
+}
+
void WebContentsBasedCanceller::SetCancelCallback(
CancelCallback cancel_callback) {
CHECK(cancel_callback_.is_null());
diff --git a/content/browser/web_contents_based_canceller.h b/content/browser/web_contents_based_canceller.h
index 9456ae9..c12b42b 100644
--- a/content/browser/web_contents_based_canceller.h
+++ b/content/browser/web_contents_based_canceller.h
@@ -35,11 +35,12 @@
// something that is incompatible with back/forward-cache, e.g. screen
// capture should not occur while in back/forward-cache.
kActiveState = 0,
- // Cancel if `WebContents::GetVisibility` is `HIDDEN`. Use this for
- // something that is incompatible with switching tabs, e.g. a modal system
- // dialog like a file-chooser that displays on top of everything should be
- // dismissed if the tab is no longer the visible tab. This also implies
- // `kActiveState` because an inactive RFH won't be visible.
+ // Cancel if `WebContents::GetVisibility` is `HIDDEN` or the WebContents is
+ // not the active tab in the browser. Use this for something that is
+ // incompatible with switching tabs, e.g. a modal system dialog like a
+ // file-chooser that displays on top of everything should be dismissed if
+ // the tab is no longer the visible tab. This also implies `kActiveState`
+ // because an inactive RFH won't be visible.
kVisibility = 1,
};
// If the cancel condition is already true, this returns `nullptr`. Otherwise
@@ -63,6 +64,7 @@
bool CanShow();
bool CanShowForVisibility(Visibility visibility);
bool CanShowForRFHActiveState();
+ bool CanShowForTabState();
// WebContentsObserver
void OnVisibilityChanged(Visibility visibility) override;
diff --git a/content/common/features.cc b/content/common/features.cc
index d9fdcd1..f6b38a04 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -606,6 +606,10 @@
BASE_FEATURE(kServiceWorkerClientUrlIsCreationUrl,
base::FEATURE_ENABLED_BY_DEFAULT);
+// Handles blocking the file picker when a visible but inactive tab in a split
+// triggers it. This serves as a kill switch for crbug.com/444653104.
+BASE_FEATURE(kSideBySideFilePickerCancelling, base::FEATURE_ENABLED_BY_DEFAULT);
+
// Enables skipping the early call to CommitPending when navigating away from a
// crashed frame.
BASE_FEATURE(kSkipEarlyCommitPendingForCrashedFrame,
diff --git a/content/common/features.h b/content/common/features.h
index 5b6d84c..762c98a 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -184,6 +184,7 @@
CONTENT_EXPORT BASE_DECLARE_FEATURE(
kServiceWorkerStaticRouterStartServiceWorker);
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 3b618e1..078c7d2 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
@@ -716,7 +716,9 @@
" })})();"));
// Top-level page in first window picks files and sends it to iframe.
- first_party_web_contents->WasShown();
+ browser()->tab_strip_model()->ActivateTabAt(
+ browser()->tab_strip_model()->GetIndexOfWebContents(
+ first_party_web_contents));
EXPECT_EQ(test_file.BaseName().AsUTF8Unsafe(),
content::EvalJs(first_party_web_contents,
"(async () => {"
@@ -882,7 +884,9 @@
" })})();"));
// Top-level page in first window picks files and sends it to iframe.
- first_party_web_contents->WasShown();
+ browser()->tab_strip_model()->ActivateTabAt(
+ browser()->tab_strip_model()->GetIndexOfWebContents(
+ first_party_web_contents));
EXPECT_EQ(test_file.BaseName().AsUTF8Unsafe(),
content::EvalJs(first_party_web_contents,
"(async () => {"
diff --git a/content/browser/file_system_access/file_system_chooser_browsertest.cc b/content/browser/file_system_access/file_system_chooser_browsertest.cc
index 7aa930a..2bddf3e 100644
--- a/content/browser/file_system_access/file_system_chooser_browsertest.cc
+++ b/content/browser/file_system_access/file_system_chooser_browsertest.cc
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <memory>
#include <string>
#include <vector>
@@ -25,6 +26,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
@@ -2197,6 +2199,34 @@
EXPECT_EQ(recorder.state, SelectFileDialogRecorder::kNotCreated);
}
+class InactiveWebContentsDelegate : public content::WebContentsDelegate {
+ bool IsContentsActive(WebContents* wc) override { return false; }
+};
+
+IN_PROC_BROWSER_TEST_F(FileSystemChooserBrowserTest, DontShowWhileInactive) {
+ GURL url = embedded_test_server()->GetURL("/title1.html");
+ ASSERT_TRUE(NavigateToURL(shell(), url));
+
+ // Record the state of the dialog.
+ SelectFileDialogRecorder recorder;
+ ui::SelectFileDialog::SetFactory(
+ std::make_unique<ObservableSelectFileDialogFactory>(
+ recorder.GetWeakPtr()));
+
+ // Provide a WebContentsDelegate that marks all web contents as inactive.
+ WebContents* wc = shell()->web_contents();
+ std::unique_ptr<InactiveWebContentsDelegate> mock =
+ std::make_unique<InactiveWebContentsDelegate>();
+ wc->SetDelegate(mock.get());
+
+ // JS should see the dialog as aborted.
+ EXPECT_EQ(
+ "AbortError",
+ content::EvalJs(wc, "window.showOpenFilePicker().catch(e => e.name)"));
+ // The dialog should not have been created.
+ EXPECT_EQ(recorder.state, SelectFileDialogRecorder::kNotCreated);
+}
+
// TODO(crbug.com/457495639): We need a different way to detect when a
// WebContents is no longer displayed to the user for android since the
// intent to select a file always causes a HIDDEN event as the whole app
Original Bug Report
File picker dialog can be shown over on different tab when focused on it (on split view)
VULNERABILITY DETAILS
the vulnerability is similar as https://issues.chromium.org/issues/40063021 but this bug occurs when the tab focus changes to the neighboring tab (happens in split view)
VERSION Version 142.0.7409.0 (Official Build) canary (64-bit) Operating System: Windows 11
REPRODUCTION CASE
- Open uyx.html
- right click on link 3 select open in split view