CVE-2025-11210
Overview
Background
- Split view (SxS)
- a Chrome desktop feature that shows two tabs side by side in one browser window. Both
WebContentsare visible, but only one is the tab strip’s active tab. - File chooser gating
WebContentsImpl’s file chooser path (reached when a page activates<input type=file>or similar) already refuses requests from background, non-visible tabs, so the user knows which site will receive the selected files.- `WebContentsDelegate`
- the embedder interface that
WebContentsImpluses to ask the browser UI (Browserin Chrome) about tab state. Its default implementations apply toWebContentsoutside a tabbed browser. - `IsContentsActive`
- a new delegate query that returns whether a
WebContentsis the active tab in its browser’s tab strip, which is different from whether it is visible.
Root Cause Analysis
The file chooser guard in web_contents_impl.cc checked only whether the requesting tab was in the background, i.e. not visible. The enclosing function’s signature is outside the hunk; the regression test calls WebContentsImpl::RunFileChooser. That check assumed visible meant active, which split view breaks: the inactive tab in a split pair is fully visible but is not the tab strip’s active tab. An inactive split tab could therefore open a native file picker. The user would reasonably assume the picker belongs to the focused or active site, and could hand files to the other site.
The patch adds a virtual WebContentsDelegate::IsContentsActive (default true) and overrides it in Browser::IsContentsActive to compare contents with tab_strip_model_->GetActiveWebContents(). It also adds an early return in the chooser path, after the existing background-tab check and before the single-active-chooser check, whenever delegate_->IsContentsActive(this) is false. The browser test FileChooserBlockedFromInactiveButVisibleWebContents confirms that a VISIBLE but inactive WebContents now has its listener cancelled and no chooser shown.
IsContentsActive) through the delegate, so only the tab strip’s active tab can open a file picker.Attack Path
- Victim uses split view The user puts an attacker-controlled page next to another site in Chrome’s split view, so the attacker’s tab is visible but not active.
- Inactive tab requests a file chooser
The attacker page triggers a file input. The request reaches the
WebContentsImplchooser path, which before the patch passed the background-tab check because the tab was visible. - Picker misattributed The native file picker opens while the user is focused on the other (active) site, so the user may believe that site asked for the file (inferred).
- Files delivered to the wrong origin
The user picks a file meant for the active site, and its contents go to the inactive attacker page. The patch blocks step 2 by returning early when
IsContentsActiveis false.
Impact Assessment
true from IsContentsActive and are unaffected.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/web_contents/web_contents_impl.cc |
modified | |
InactiveContentsDelegatecontent/browser/web_contents/web_contents_impl_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/browser.ccchrome/browser/ui/browser.hcontent/browser/web_contents/web_contents_impl.cccontent/browser/web_contents/web_contents_impl_browsertest.cccontent/public/browser/web_contents_delegate.cccontent/public/browser/web_contents_delegate.h
Audit Directions
- Visibility used as an authorization proxySearch for other privileged prompts and APIs gated on
GetVisibility()or “not hidden” (for exampleEnumerateDirectory, permission prompts,showOpenFilePicker, color and date pickers, downloads, fullscreen) that split view can now reach from an inactive but visible tab. - Delegate defaultsCheck that
IsContentsActive’s defaulttruecannot be reached by tabbed embedders, such as app windows or otherWebContentsDelegateimplementations, where several visible contents also exist. - Prompt anchoring in multi-pane UIsConfirm that dialogs which carry a security decision show the origin of the pane that asked, and are anchored to it, when two tabs share a window.
Patch
From d4c4449f8891183a1afc71a15b8b9b6596a5aad3 Mon Sep 17 00:00:00 2001 From: Alison Gale <[email protected]> Date: Mon, 08 Sep 2025 16:21:45 -0700 Subject: [PATCH] [SxS] Prevent opening file picker from inactive tab in split For security purposes, only the active tab should be able to open a filed picker so the user knows which site is receiving the files. Change-Id: If73e028c4041b40d2cfc935cb37f2b61b32416fd Bug: 441139337,440523110 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6916005 Reviewed-by: Eshwar Stalin <[email protected]> Commit-Queue: Alison Gale <[email protected]> Reviewed-by: Nasko Oskov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1512743} --- diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc index 96f4531..b48603f9 100644 --- a/chrome/browser/ui/browser.cc +++ b/chrome/browser/ui/browser.cc @@ -2222,6 +2222,10 @@ window_->Activate(); } +bool Browser::IsContentsActive(content::WebContents* contents) { + return tab_strip_model_->GetActiveWebContents() == contents; +} + void Browser::LoadingStateChanged(WebContents* source, bool should_show_loading_ui) { ScheduleUIUpdate(source, content::INVALIDATE_TYPE_LOAD); diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h index cb5164b..e7c3c7ca 100644 --- a/chrome/browser/ui/browser.h +++ b/chrome/browser/ui/browser.h @@ -696,6 +696,7 @@ // Overridden from content::WebContentsDelegate: void ActivateContents(content::WebContents* contents) override; + bool IsContentsActive(content::WebContents* contents) override; void SetTopControlsShownRatio(content::WebContents* web_contents, float ratio) override; int GetTopControlsHeight() override; diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc index 6dc6fedc..794678d 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc @@ -8942,6 +8942,10 @@ // Do not allow background tab to open file chooser. return; } + if (!delegate_->IsContentsActive(this)) { + // Do not allow inactive tabs to open file chooser. + return; + } if (active_file_chooser_) { // Only allow one active file chooser at one time. return; diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc index 36fa97c..9a54a68 100644 --- a/content/browser/web_contents/web_contents_impl_browsertest.cc +++ b/content/browser/web_contents/web_contents_impl_browsertest.cc @@ -3152,6 +3152,37 @@ EXPECT_EQ(shell()->run_file_chooser_count(), 0u); } +class InactiveContentsDelegate : public WebContentsDelegate { + public: + explicit InactiveContentsDelegate(WebContents* contents) { + contents->SetDelegate(this); + } + + bool IsContentsActive(content::WebContents* web_contents) override { + return false; + } +}; + +IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest, + FileChooserBlockedFromInactiveButVisibleWebContents) { + ASSERT_TRUE(embedded_test_server()->Start()); + shell()->set_hold_file_chooser(); + + EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); + + WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents()); + EXPECT_EQ(shell()->web_contents()->GetVisibility(), Visibility::VISIBLE); + InactiveContentsDelegate delegate(wc); + + auto [chooser, remote] = + FileChooserImpl::CreateForTesting(wc->GetPrimaryMainFrame()); + auto file_select_listener = base::MakeRefCounted<MockFileSelectListener>(); + wc->RunFileChooser(chooser->GetWeakPtr(), wc->GetPrimaryMainFrame(), + file_select_listener, blink::mojom::FileChooserParams()); + EXPECT_TRUE(file_select_listener->cancelled()); + EXPECT_EQ(shell()->run_file_chooser_count(), 0u); +} + IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest, EnumerateDirectoryBlockedFromHiddenWebContents) { ASSERT_TRUE(embedded_test_server()->Start()); diff --git a/content/public/browser/web_contents_delegate.cc b/content/public/browser/web_contents_delegate.cc index e4d6967..87e310a5 100644 --- a/content/public/browser/web_contents_delegate.cc +++ b/content/public/browser/web_contents_delegate.cc @@ -57,6 +57,10 @@ return nullptr; } +bool WebContentsDelegate::IsContentsActive(WebContents* contents) { + return true; +} + bool WebContentsDelegate::CanOverscrollContent() { return false; } diff --git a/content/public/browser/web_contents_delegate.h b/content/public/browser/web_contents_delegate.h index 726ae29..e02c45b 100644 --- a/content/public/browser/web_contents_delegate.h +++ b/content/public/browser/web_contents_delegate.h @@ -194,6 +194,14 @@ // Selects the specified contents, bringing its container to the front. virtual void ActivateContents(WebContents* contents) {} + // A WebContents within a browser is considered active if it is the "active" + // tab in the browser's tab strip. A non-active WebContents cannot have focus, + // but it is possible for an active WebContents to not be focused if focus is + // elsewhere in the browser.Just because a WebContents is visible, doesn't + // mean it is active due to the SplitView feature. WebContents outside a + // browser are always considered active. + virtual bool IsContentsActive(WebContents* contents); + // Notifies the delegate that this contents is starting or is done loading // some resource. The delegate should use this notification to represent // loading feedback. See WebContents::IsLoading()
Regression Test / PoC
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index 36fa97c..9a54a68 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -3152,6 +3152,37 @@
EXPECT_EQ(shell()->run_file_chooser_count(), 0u);
}
+class InactiveContentsDelegate : public WebContentsDelegate {
+ public:
+ explicit InactiveContentsDelegate(WebContents* contents) {
+ contents->SetDelegate(this);
+ }
+
+ bool IsContentsActive(content::WebContents* web_contents) override {
+ return false;
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest,
+ FileChooserBlockedFromInactiveButVisibleWebContents) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+ shell()->set_hold_file_chooser();
+
+ EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+
+ WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
+ EXPECT_EQ(shell()->web_contents()->GetVisibility(), Visibility::VISIBLE);
+ InactiveContentsDelegate delegate(wc);
+
+ auto [chooser, remote] =
+ FileChooserImpl::CreateForTesting(wc->GetPrimaryMainFrame());
+ auto file_select_listener = base::MakeRefCounted<MockFileSelectListener>();
+ wc->RunFileChooser(chooser->GetWeakPtr(), wc->GetPrimaryMainFrame(),
+ file_select_listener, blink::mojom::FileChooserParams());
+ EXPECT_TRUE(file_select_listener->cancelled());
+ EXPECT_EQ(shell()->run_file_chooser_count(), 0u);
+}
+
IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest,
EnumerateDirectoryBlockedFromHiddenWebContents) {
ASSERT_TRUE(embedded_test_server()->Start());
Original Bug Report
(Split View) UI spoofing Upload leads leaking confidential files, Photos to Attacker
Steps to reproduce the problem
- Enable the #side-by-side flag
- Open poc.html in Chrome Browser
- Right click and open in Split view
- This causes the Attacker file dialog to on omnibox e.g.: drive.google.com and then Select any file or photo
- Successfully Uploaded in Attacker site
Problem Description
When the user performs a right-click, this malicious listener is activated, and it displays the attacker’s fabricated file upload dialog with Omnibox spoof, The file dialog itself isn’t coming from Google’s official website, but rather from an Attacker site
This is the primary goal. By convincing a user that they are on a legitimate website using Omnibox spoofing
This causes the Attacker page file dialog to open in the middle of the screen and above omnibox showing legitimate website Eg: https://drive.google.com/drive/ that was just opened and might confuse the user into leaking sensitive information (their Photos, confidential files) to an attacker. This can make user into Believing it was opened/requested by the “drive.google.com” tab.
the user’s most critical security checkpoint: the address bar. The user believes they are on a safe, legitimate domain, which dramatically increases the likelihood of a successful attack
Summary
(Split View) UI spoofing Upload leads leaking confidential files, Photos to Attacker
Additional Data
Category: Security
Chrome Channel: Beta
Regression: N/A \