CVE-2025-5065
Overview
Background
- File System Access API
- the web API (
showOpenFilePicker/showSaveFilePicker/showDirectoryPicker) whose picker launch is mediated in the browser process bycontent/browser/file_system_access/file_system_access_manager_impl.cc. - Transient user activation
- a short-lived, gesture-derived state indicating the user recently interacted with the page, required to gate privileged actions like opening a file/directory picker.
- Consuming activation
- spending the transient activation so it cannot be reused for a second privileged action, versus merely checking it and leaving it available.
- `FrameTreeNode::UpdateUserActivationState`
- the browser-process method used here to consume transient activation via
kConsumeTransientActivation.
Root Cause Analysis
The File System Access picker path in file_system_access_manager_impl.cc required transient user activation but only checked it; it never consumed it, so a single user gesture could satisfy the activation requirement for more than one picker invocation. Because the existing activation check lives in the browser process, consuming activation in the renderer would have made that browser-side check fail (as the commit message explains), so the pre-patch code left activation intact after passing the check.
The fix adds, before the existing “already active file picker” guard, a block that consumes activation in the browser process: if content_browser_client->IsTransientActivationRequiredForShowFileOrDirectoryPicker(web_contents) is true, it calls FrameTreeNode::From(rfh)->UpdateUserActivationState(kConsumeTransientActivation, kNone). This enforces the intended one-gesture-per-picker invariant so the activation cannot be silently reused. The change is a tightening; it consumes activation on each qualifying call rather than leaving it available for subsequent invocations, and the added frame_tree_node.h include supports the new FrameTreeNode call.
UpdateUserActivationState(kConsumeTransientActivation, ...) so each launch needs its own gesture.Attack Path
- Obtain one user gesture A page gets a single legitimate user interaction that grants transient activation.
- Launch a picker Script calls a File System Access picker method; the browser-process check confirms activation is present and (pre-patch) leaves it intact.
- Reuse the same activation Because activation was never consumed, script can invoke the picker again (or another activation-gated action) on the same gesture, defeating the one-gesture requirement.
- Abuse repeated/unexpected prompts The attacker leverages the un-consumed activation to trigger picker behavior the user did not separately authorize, the “inappropriate implementation” the advisory describes. (inferred user-facing abuse such as spoofing/annoyance)
Impact Assessment
(inferred).Files Changed
content/browser/file_system_access/file_system_access_manager_impl.cc
Audit Directions
- Check-without-consume activationAudit other activation-gated browser-process actions (pickers, permission prompts, downloads, popups) to confirm they consume transient activation rather than merely checking it, preventing gesture reuse.
- Renderer/browser activation splitLook for features whose activation check runs in the browser process while consumption (if any) happens in the renderer, since the two can drift and leave activation un-consumed.
- File System Access entry pointsReview each
showOpenFilePicker/showSaveFilePicker/showDirectoryPickerand related privileged entry point for consistent activation consumption and for the browser-to-renderer notification gap noted in the fix’s TODO.
Patch
From d5561f9e792bef4da69074b13cc04bbf38f95d69 Mon Sep 17 00:00:00 2001
From: Joey Arhar <jarhar@chromium.org>
Date: Fri, 18 Apr 2025 08:57:42 -0700
Subject: [PATCH] Make FileSystemAccess APIs consume user activation
User activation is consumed in the browser process instead of the
renderer process like input.showPicker() because consuming user
activation in the renderer would make the existing user activation check
in the browser process fail.
There is no browsertest because content::EvalJs() sets user activation
after running script, even with the flag to not run user activation, and
I don't know how to make it stop.
There is no web_test because consuming user activation in the browser
process does not notify the renderer process that it lost user
activation. It does not look like there is any existing interface to
make the browser tell the renderer that it lost user activation, only
the other way around.
I manually verified that the attached bug is fixed.
Spec issue: https://github.com/WICG/file-system-access/issues/458
Fixed: 40059071
Change-Id: Ia1f5ff63a8bdf6cdec70dea1406e9865123f96a5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5735618
Reviewed-by: Mustaq Ahmed <mustaq@chromium.org>
Reviewed-by: Fergal Daly <fergal@chromium.org>
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1448885}
---
diff --git a/content/browser/file_system_access/file_system_access_manager_impl.cc b/content/browser/file_system_access/file_system_access_manager_impl.cc
index b5bd63d..9387190f 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl.cc
+++ b/content/browser/file_system_access/file_system_access_manager_impl.cc
@@ -49,6 +49,7 @@
#include "content/browser/file_system_access/file_system_access_watcher_manager.h"
#include "content/browser/file_system_access/file_system_chooser.h"
#include "content/browser/file_system_access/fixed_file_system_access_permission_grant.h"
+#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
@@ -583,6 +584,18 @@
return;
}
+ // Consume user activation to address this issue: crbug.com/40059071
+ // TODO(crbug.com/411125804): Consider moving this user activation check to
+ // the renderer process or informing the renderer that it lost user
+ // activation.
+ if (content_browser_client
+ ->IsTransientActivationRequiredForShowFileOrDirectoryPicker(
+ web_contents)) {
+ FrameTreeNode::From(rfh)->UpdateUserActivationState(
+ blink::mojom::UserActivationUpdateType::kConsumeTransientActivation,
+ blink::mojom::UserActivationNotificationType::kNone);
+ }
+
// Don't show the file picker if there is an already active file picker for
// this render frame host.
GlobalRenderFrameHostId global_rfh_id = rfh->GetGlobalId();
Original Bug Report
File picker UI spoof
UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Steps to reproduce the problem: onmouseup = _ => { let fs = showOpenFilePicker(); open(‘https://www.google.com’); }
Click
What is the expected behavior? For open() to require its own user interaction. (it also allows for a window popunder) Stuff like window.close location.href should lose the FileSystemHandle anyway.
What went wrong? File picker shown on the wrong origin while keeping FileSystemHandle.
Did this work before? N/A
Chrome version: 99.0.4844.51 Channel: stable OS Version: 10.0