Medium watch CVSS 5.4 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactSide-channel information leakage in Tab
DescriptionSide-channel information leakage in Tab
ComponentTab
Sub-componentSxS
Bug ClassInfo Disclosure
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker440523110
Fix commitd4c4449f8891 (chromium/src) +52/-0
CWECWE-1300
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedUmar Farooq
Reported2025-08-22 · bounty $3000 (per vendor advisory)
Fix landed2025-09-08 (public git)
Disclosed2025-09-30
Patch gap22 days — fix public in source before the advisory

Background

Split view (SxS)
a Chrome desktop feature that shows two tabs side by side in one browser window. Both WebContents are 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 WebContentsImpl uses to ask the browser UI (Browser in Chrome) about tab state. Its default implementations apply to WebContents outside a tabbed browser.
`IsContentsActive`
a new delegate query that returns whether a WebContents is 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.

Key insight
The file chooser permission used visibility as a stand-in for “the tab the user is interacting with”, and split view makes those two things different. The fix adds an explicit active-tab check (IsContentsActive) through the delegate, so only the tab strip’s active tab can open a file picker.

Attack Path

  1. 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.
  2. Inactive tab requests a file chooser The attacker page triggers a file input. The request reaches the WebContentsImpl chooser path, which before the patch passed the background-tab check because the tab was visible.
  3. 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).
  4. 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 IsContentsActive is false.

Impact Assessment

The result is disclosure of user-selected local files to an unintended origin through UI confusion, in the browser and renderer of the attacker’s tab. There is no memory corruption. The vendor rates the bug medium (“Side-channel information leakage in Tab”) and gives no in-the-wild exploitation status. Preconditions: the split view feature must be available and in use with the attacker’s tab as the inactive pane, the page must trigger a file chooser (normally needing a user activation, inferred), and the user must be fooled into choosing a sensitive file. No compromised renderer is needed, because ordinary web content can request a file chooser. Embedders without a tab strip get the default true from IsContentsActive and are unaffected.

Changed Functions

FunctionChangeNotes
if
content/browser/web_contents/web_contents_impl.cc
modified
InactiveContentsDelegate
content/browser/web_contents/web_contents_impl_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/browser.cc
  • chrome/browser/ui/browser.h
  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/web_contents/web_contents_impl_browsertest.cc
  • content/public/browser/web_contents_delegate.cc
  • content/public/browser/web_contents_delegate.h

Audit Directions

  • Visibility used as an authorization proxy
    Search for other privileged prompts and APIs gated on GetVisibility() or “not hidden” (for example EnumerateDirectory, permission prompts, showOpenFilePicker, color and date pickers, downloads, fullscreen) that split view can now reach from an inactive but visible tab.
  • Delegate defaults
    Check that IsContentsActive’s default true cannot be reached by tabbed embedders, such as app windows or other WebContentsDelegate implementations, where several visible contents also exist.
  • Prompt anchoring in multi-pane UIs
    Confirm 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.
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()
Loading diff…

Regression Test / PoC

shipped with the fix
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());
Loading diff…

Original Bug Report

reported by [email protected]

(Split View) UI spoofing Upload leads leaking confidential files, Photos to Attacker

Steps to reproduce the problem

  1. Enable the #side-by-side flag
  2. Open poc.html in Chrome Browser
  3. Right click and open in Split view
  4. This causes the Attacker file dialog to on omnibox e.g.: drive.google.com and then Select any file or photo
  5. 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 \

View on issue tracker
Uploaded PoC files
Links in the report