CVE-2026-13981
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm |
modified |
Files Changed
ios/chrome/browser/web/model/choose_file/choose_file_tab_helper.mmios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm
Patch
From e0130db934447a47b212d206ae12f0696ea3c64c Mon Sep 17 00:00:00 2001
From: Quentin Pubert <qpubert@google.com>
Date: Tue, 19 May 2026 09:06:21 -0700
Subject: [PATCH] [iOS] Prevent stale ChooseFileEvent persistence in ChooseFileTabHelper
Stale file-choosing metadata in ChooseFileTabHelper can persist across
cross-document navigations. An attacker can pre-seed this state to
influence the native file picker behavior on a victim origin, leading to
potential UI spoofing or policy overrides.
This CL updates ChooseFileTabHelper to reset the cached last
ChooseFileEvent whenever a cross-document navigation starts or when the
tab is hidden.
Fixed: 513990408
Change-Id: I5740cd295e7691e5f669c89faea58eee2ebc930d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859612
Reviewed-by: Olivier Robin <olivierrobin@chromium.org>
Auto-Submit: Quentin Pubert <qpubert@google.com>
Commit-Queue: Olivier Robin <olivierrobin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632880}
---
diff --git a/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper.mm b/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper.mm
index 5ad4eb7..33c1ff3 100644
--- a/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper.mm
+++ b/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper.mm
@@ -231,11 +231,13 @@
web::NavigationContext* navigation_context) {
if (!navigation_context->IsSameDocument()) {
AbortSelection();
+ ResetLastChooseFileEvent();
}
}
void ChooseFileTabHelper::WasHidden(web::WebState* web_state) {
AbortSelection();
+ ResetLastChooseFileEvent();
}
void ChooseFileTabHelper::WebStateDestroyed(web::WebState* web_state) {
diff --git a/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm b/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm
index 56227b9..aff35102 100644
--- a/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm
+++ b/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm
@@ -191,6 +191,48 @@
EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
}
+// Tests that starting navigation to a different document resets the last
+// ChooseFileEvent.
+TEST_F(ChooseFileTabHelperTest, DidStartNavigationResetsLastChooseFileEvent) {
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+
+ ChooseFileEvent event = ChooseFileEvent::Builder()
+ .SetAllowMultipleFiles(false)
+ .SetHasSelectedFile(false)
+ .SetWebState(web_state_.get())
+ .Build();
+ tab_helper_->SetLastChooseFileEvent(event);
+ EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());
+
+ auto navigation_context = std::make_unique<web::FakeNavigationContext>();
+
+ // Same document navigation should NOT reset last_choose_file_event_.
+ navigation_context->SetIsSameDocument(true);
+ tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
+ EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());
+
+ // Cross-document navigation should reset last_choose_file_event_.
+ navigation_context->SetIsSameDocument(false);
+ tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+}
+
+// Tests that hiding the tab resets the last ChooseFileEvent.
+TEST_F(ChooseFileTabHelperTest, WasHiddenResetsLastChooseFileEvent) {
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+
+ ChooseFileEvent event = ChooseFileEvent::Builder()
+ .SetAllowMultipleFiles(false)
+ .SetHasSelectedFile(false)
+ .SetWebState(web_state_.get())
+ .Build();
+ tab_helper_->SetLastChooseFileEvent(event);
+ EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());
+
+ tab_helper_->WasHidden(web_state_.get());
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+}
+
// Tests that `RunOpenPanel()` starts file selection in the tab and shows the
// file upload panel.
TEST_F(ChooseFileTabHelperTest, RunOpenPanel) {
Regression Test / PoC
diff --git a/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm b/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm
index 56227b9..aff35102 100644
--- a/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm
+++ b/ios/chrome/browser/web/model/choose_file/choose_file_tab_helper_unittest.mm
@@ -191,6 +191,48 @@
EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
}
+// Tests that starting navigation to a different document resets the last
+// ChooseFileEvent.
+TEST_F(ChooseFileTabHelperTest, DidStartNavigationResetsLastChooseFileEvent) {
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+
+ ChooseFileEvent event = ChooseFileEvent::Builder()
+ .SetAllowMultipleFiles(false)
+ .SetHasSelectedFile(false)
+ .SetWebState(web_state_.get())
+ .Build();
+ tab_helper_->SetLastChooseFileEvent(event);
+ EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());
+
+ auto navigation_context = std::make_unique<web::FakeNavigationContext>();
+
+ // Same document navigation should NOT reset last_choose_file_event_.
+ navigation_context->SetIsSameDocument(true);
+ tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
+ EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());
+
+ // Cross-document navigation should reset last_choose_file_event_.
+ navigation_context->SetIsSameDocument(false);
+ tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+}
+
+// Tests that hiding the tab resets the last ChooseFileEvent.
+TEST_F(ChooseFileTabHelperTest, WasHiddenResetsLastChooseFileEvent) {
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+
+ ChooseFileEvent event = ChooseFileEvent::Builder()
+ .SetAllowMultipleFiles(false)
+ .SetHasSelectedFile(false)
+ .SetWebState(web_state_.get())
+ .Build();
+ tab_helper_->SetLastChooseFileEvent(event);
+ EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());
+
+ tab_helper_->WasHidden(web_state_.get());
+ EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
+}
+
// Tests that `RunOpenPanel()` starts file selection in the tab and shows the
// file upload panel.
TEST_F(ChooseFileTabHelperTest, RunOpenPanel) {
Original Bug Report
Potential persistence of stale file-choosing metadata in ChooseFileTabHelper on iOS
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The ChooseFileTabHelper on iOS caches file-choosing metadata that is not cleared during cross-document navigations. An attacker can pre-seed this state to influence the native file picker behavior on a victim origin, leading to potential UI spoofing or policy overrides.
Affected files:
ios/chrome/browser/web/model/choose_file/choose_file_tab_helper.mmios/chrome/browser/web/model/choose_file/choose_file_java_script_feature.mmios/chrome/browser/web/model/choose_file/resources/choose_file.ts
Estimated timestamp from git blame: 2025-10-03
Vulnerability Summary
A potential logic vulnerability in ChooseFileTabHelper on iOS allows file-choosing metadata to persist across cross-document navigations. This state can be exploited by a malicious website to influence the parameters of a file picker subsequently triggered on a different origin.
Technical Details
When a user interacts with a file input on iOS Chrome (when the kIOSCustomFileUploadMenu feature is enabled), the ChooseFileJavaScriptFeature sends a ChooseFileHandler message from the page to the browser. This message contains metadata such as the capture attribute, accepted MIME types/extensions, and the screen location of the interaction. This metadata is stored in the last_choose_file_event_ member of the ChooseFileTabHelper class.
The ChooseFileTabHelper acts as a web::WebStateObserver and implements DidStartNavigation. However, its current implementation only clears the active ChooseFileController and does not reset the cached last_choose_file_event_ member:
// ios/chrome/browser/web/model/choose_file/choose_file_tab_helper.mm
void ChooseFileTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (!navigation_context->IsSameDocument()) {
AbortSelection(); // Resets the controller_, but leaves last_choose_file_event_ intact
}
}
If a user navigates from an attacker-controlled page to a victim page, any metadata pre-seeded by the attacker remains in the tab helper’s state. When a file picker is subsequently triggered on the victim page, the browser’s RunOpenPanel method consumes this cached event. While RunOpenPanel overwrites some fields (like allowsMultipleSelection) using values from the native WKOpenPanelParameters, other sensitive fields like capture and accept types are not part of the native parameters and are thus used directly from the stale, attacker-provided event.
Potential Exploitation Path
An attacker could potentially trigger this vulnerability using the following suggested steps:
- The attacker’s site sends a
ChooseFileHandlermessage via JavaScript with malicious metadata (e.g., settingcapture: 1to force a direct camera launch) and then navigates the tab to a victim site (e.g.,https://victim.example/upload). - On the victim site, the user interacts with a file input in a way that bypasses the browser’s JavaScript interception logic. This can occur if the input is within a Shadow DOM (where event retargeting prevents the browser’s listener from identifying the target as an
HTMLInputElement) or if the site uses theshowPicker()API, which is not intercepted by the current prototype patches. - The browser’s
RunOpenPanelretrieves the stale metadata from the attacker’s origin and uses it to configure the native file picker on the victim’s origin. - The native UI behavior is driven by the attacker’s values, potentially resulting in the camera opening directly or restricted file types being displayed without the victim origin’s consent.
Suggested Fix
The DidStartNavigation method in ChooseFileTabHelper should be updated to clear the last_choose_file_event_ whenever a cross-document navigation occurs. This ensures that metadata from a previous origin cannot influence the file picker on a new origin.
void ChooseFileTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (!navigation_context->IsSameDocument()) {
AbortSelection();
ResetLastChooseFileEvent(); // Suggested addition to clear the cached event
}
}
Note: These findings are based on manual code analysis; a functional proof-of-concept has not been executed.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.