Low watch CVSS 9.8 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Split View
DescriptionIncorrect security UI in Split View
ComponentSplit View
Sub-componentSxS
Bug ClassSpoofing
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.
Tracker444653104
Fix commitc72906f92191 (chromium/src) +68/-8
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedHafiizh
Reported2025-09-12 · bounty $500 (per vendor advisory)
Fix landed2025-11-13 (public git)
Disclosed2026-01-13
Patch gap61 days — fix public in source before the advisory

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 WebContents can be visible while it is not the active tab.
`WebContentsBasedCanceller`
a content/ helper (a WebContentsObserver) that decides whether a browser-shown dialog such as the File System Access file chooser may be shown, and cancels it when its CancelCondition becomes 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() is HIDDEN” (plus an inactive RenderFrameHost).
`WebContentsDelegate::IsContentsActive`
the embedder hook that reports whether a given WebContents is 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.

Key insight
The file-chooser gating treated 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

  1. 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.
  2. Picker requested from inactive pane Script in the attacker’s visible-but-inactive tab calls a File System Access picker such as window.showOpenFilePicker().
  3. Check passes pre-patch Because the tab’s visibility is not HIDDEN and its frame is active, CanShowForVisibility and CanShowForRFHActiveState both allow it, and the native file dialog is shown.
  4. 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

A web page in the non-active pane of a Split View could present a browser file chooser that the user may attribute to the other, active tab, resulting in incorrect security UI and potentially leaking user-chosen files to the wrong origin (inferred). Preconditions are that the user is using Split View on desktop Chrome with the attacker page in the inactive pane, and that the user interacts with the dialog; File System Access pickers normally require transient user activation in the requesting frame, which is not shown in this diff (inferred). The vendor rates this as low severity (“Incorrect security UI in Split View”); the advisory provided does not state in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
InactiveWebContentsDelegate
content/browser/file_system_access/file_system_chooser_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/file_system_access/file_system_chooser_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/views/file_system_access/file_system_access_browsertest.cc
  • content/browser/file_system_access/file_system_chooser_browsertest.cc
  • content/browser/web_contents_based_canceller.cc
  • content/browser/web_contents_based_canceller.h
  • content/common/features.cc
  • content/common/features.h

Audit Directions

  • Visibility used as a proxy for focus
    Search for other permission prompts, choosers, and modal dialogs that gate on WebContents::GetVisibility() or OnVisibilityChanged alone and may misbehave when two tabs are simultaneously visible in Split View.
  • Request-time-only checks
    As 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
    CanShowForTabState allows the dialog when GetDelegate() returns null; review embedders and contexts (e.g. tabs being moved between windows) where the delegate may be absent or transient.
From c72906f92191aa760ae462ad5d879ab95ebbc472 Mon Sep 17 00:00:00 2001
From: Alison Gale <[email protected]>
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 <[email protected]>
Reviewed-by: Bo Liu <[email protected]>
Reviewed-by: Charlie Reis <[email protected]>
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);
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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

  1. Open uyx.html
  2. right click on link 3 select open in split view
View on issue tracker