CVE-2026-7964
Overview
Background
- `FileSystemManagerImpl::Open`
- the browser-process (
content) handler for theblink.mojom.FileSystemManagerOpenMojo IPC, which opens a sandboxed filesystem for a given origin and runs an access check before proceeding. - Renderer-supplied origin
- an
url::Originpassed up from the (untrusted, potentially compromised) renderer in the IPC, as opposed to the browser-authoritative origin the browser already knows for that Mojo connection. - `receivers_.current_context().origin()`
- the browser-side origin bound to the Mojo receiver when it was added (
storage_key), i.e. the trustworthy origin the browser attributes to the calling frame/worker. - `CanAccessDataForOrigin`
- the
ChildProcessSecurityPolicyImpl::Handlecheck (run on the UI thread) that verifies the renderer process is actually allowed to act for the origin being opened.
Root Cause Analysis
Historically Open accepted an origin parameter straight from the renderer and used it (origin_to_check = origin) as the origin to open and to feed into CanAccessDataForOrigin. A compromised renderer could therefore supply an origin it does not own, and the invariant that filesystem access must be scoped to the frame’s true, browser-authoritative origin was dependent on trusting renderer input. Step 1 of the fix (a prior CL) introduced the kEnforceFileSystemManagerOpenOrigin feature flag, which when enabled overrode origin_to_check with receivers_.current_context().origin(). This CL (step 2) completes the hardening: it removes the origin parameter entirely from the Mojo interface and all renderer call sites, deletes the now-unconditional flag, and makes Open always pass receivers_.current_context().origin() to the access check.
The fix works because the origin used for the security decision can no longer be influenced by the renderer; it is taken solely from the browser-trusted receiver context.
Attack Path
- Compromise a renderer
Gain the ability to send arbitrary
FileSystemManager.OpenIPCs from a renderer process (a standard precondition for this class of browser-process validation bug). - Forge the origin argument
In the pre-fix (flag-disabled) interface, call
Open(origin, file_system_type, ...)with anoriginthe process does not legitimately control. - Reach the access check with the forged origin
FileSystemManagerImpl::Opensetsorigin_to_check = originand forwards it toCanAccessDataForOrigin, so the decision is made against renderer-chosen input rather than the real frame origin. - Open a cross-origin sandboxed filesystem If the check is satisfied against the forged origin, the renderer obtains a filesystem name/root bound to another origin (inferred: cross-origin sandboxed-filesystem data access), violating origin isolation.
Impact Assessment
Open access check, potentially obtaining access to a sandboxed filesystem scoped to an origin it does not own (cross-origin data access, inferred). This operates at the browser-process IPC boundary and requires the ability to send crafted FileSystemManager IPCs. The advisory rates it medium and labels it “Insufficient validation of untrusted input in FileSystem”; no in-the-wild exploitation is stated, and by the time of this CL the enforcing flag was already enabled by default, so this step finalizes the removal of the untrusted parameter.Changed Functions
| Function | Change | Notes |
|---|---|---|
Blobthird_party/blink/renderer/modules/filesystem/file_system_dispatcher.h |
modified | |
ExecutionContextthird_party/blink/renderer/modules/filesystem/file_system_dispatcher.h |
modified | |
KURLthird_party/blink/renderer/modules/filesystem/file_system_dispatcher.h |
modified | |
SecurityOriginthird_party/blink/renderer/modules/filesystem/file_system_dispatcher.h |
modified |
Files Changed
content/browser/file_system/file_system_manager_impl.cccontent/browser/file_system/file_system_manager_impl.hcontent/common/features.cccontent/common/features.hthird_party/blink/public/mojom/filesystem/file_system.mojomthird_party/blink/renderer/core/fileapi/file_test.ccthird_party/blink/renderer/modules/filesystem/file_system_dispatcher.ccthird_party/blink/renderer/modules/filesystem/file_system_dispatcher.hthird_party/blink/renderer/modules/filesystem/local_file_system.cc
Audit Directions
- Renderer-supplied identity in browser IPCSearch other Mojo interfaces for origin/
StorageKey/URL parameters that duplicate information the browser already holds in the receiver context, and prefercurrent_context()over the IPC argument. - Flag-to-removal transitionsWhen a
kEnforce...feature flag reaches default-enabled, verify the untrusted parameter it was guarding is actually removed, not merely overridden, so no legacy call path can still pass it. - Access checks fed by argumentsAudit callers of
CanAccessDataForOriginand similarChildProcessSecurityPolicychecks to ensure the origin/StorageKey argument originates from browser-authoritative state rather than the message payload.
Patch
From 5c6dc2fef93b3c4618672b2885f7b53f6038d9b9 Mon Sep 17 00:00:00 2001
From: Tsuyoshi Horo <horo@chromium.org>
Date: Wed, 06 May 2026 18:41:10 -0700
Subject: [PATCH] Remove origin parameter from FileSystemManager::Open
This is step 2 of 2 for enforcing the browser-authoritative origin in
FileSystemManager::Open.
With the kEnforceFileSystemManagerOpenOrigin feature flag from step 1
(https://crrev.com/c/7717899) handling the transition, this CL removes
the origin parameter from the Mojo IPC interface and all associated
renderer calls.
Bug: 497254383
Change-Id: I41f29d395d7b0812b13b0535aad095a7622c3302
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7720200
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Commit-Queue: Tsuyoshi Horo <horo@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1626655}
---
diff --git a/content/browser/file_system/file_system_manager_impl.cc b/content/browser/file_system/file_system_manager_impl.cc
index cc9c6ae..3ffe9a4 100644
--- a/content/browser/file_system/file_system_manager_impl.cc
+++ b/content/browser/file_system/file_system_manager_impl.cc
@@ -19,7 +19,6 @@
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/file_system/browser_file_system_helper.h"
-#include "content/common/features.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/child_process_id.h"
@@ -214,17 +213,10 @@
receivers_.Add(this, std::move(receiver), storage_key);
}
-void FileSystemManagerImpl::Open(const url::Origin& origin,
- blink::mojom::FileSystemType file_system_type,
+void FileSystemManagerImpl::Open(blink::mojom::FileSystemType file_system_type,
OpenCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
- url::Origin origin_to_check = origin;
- if (base::FeatureList::IsEnabled(
- features::kEnforceFileSystemManagerOpenOrigin)) {
- origin_to_check = receivers_.current_context().origin();
- }
-
// Run the access check on the UI thread using a duplicated
// ChildProcessSecurityPolicy::Handle, ensuring the SecurityState exists when
// the task runs even if this instance and its Handle are gone at the time.
@@ -234,7 +226,7 @@
&ChildProcessSecurityPolicyImpl::Handle::CanAccessDataForOrigin,
std::make_unique<ChildProcessSecurityPolicyImpl::Handle>(
security_policy_handle_.Duplicate()),
- origin_to_check),
+ receivers_.current_context().origin()),
base::BindOnce(&FileSystemManagerImpl::ContinueOpen,
weak_factory_.GetWeakPtr(), file_system_type,
receivers_.GetBadMessageCallback(), std::move(callback),
diff --git a/content/browser/file_system/file_system_manager_impl.h b/content/browser/file_system/file_system_manager_impl.h
index 24ec7ecd..bf9ed95 100644
--- a/content/browser/file_system/file_system_manager_impl.h
+++ b/content/browser/file_system/file_system_manager_impl.h
@@ -74,8 +74,7 @@
mojo::PendingReceiver<blink::mojom::FileSystemManager> receiver);
// blink::mojom::FileSystem
- void Open(const url::Origin& origin,
- blink::mojom::FileSystemType file_system_type,
+ void Open(blink::mojom::FileSystemType file_system_type,
OpenCallback callback) override;
void ResolveURL(const GURL& filesystem_url,
ResolveURLCallback callback) override;
diff --git a/content/common/features.cc b/content/common/features.cc
index b0f87821..118dc8d 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -220,13 +220,6 @@
base::FEATURE_DISABLED_BY_DEFAULT);
#endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
-// Enforces the use of the browser-authoritative origin from the Mojo receiver
-// context instead of the renderer-supplied origin in FileSystemManager::Open.
-// TODO(crbug.com/497254383): Remove this flag and the origin parameter from
-// the Mojo interface.
-BASE_FEATURE(kEnforceFileSystemManagerOpenOrigin,
- base::FEATURE_ENABLED_BY_DEFAULT);
-
// When enabled, enforces that same-document navigations must not change
// the committed origin, insecure request policy, or insecure navigations set.
// Any mismatch will result in a renderer kill via bad_message handling.
diff --git a/content/common/features.h b/content/common/features.h
index 6a95343..1d89379 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -86,7 +86,6 @@
CONTENT_EXPORT BASE_DECLARE_FEATURE(kEnableDevToolsJsErrorReporting);
#endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
CONTENT_EXPORT BASE_DECLARE_FEATURE(kEnforceSameDocumentOriginInvariants);
-CONTENT_EXPORT BASE_DECLARE_FEATURE(kEnforceFileSystemManagerOpenOrigin);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kEmbeddingRequiresOptIn);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kExperimentalContentSecurityPolicyFeatures);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kFedCmNonStringToken);
diff --git a/third_party/blink/public/mojom/filesystem/file_system.mojom b/third_party/blink/public/mojom/filesystem/file_system.mojom
index c4033f8..32cc697 100644
--- a/third_party/blink/public/mojom/filesystem/file_system.mojom
+++ b/third_party/blink/public/mojom/filesystem/file_system.mojom
@@ -73,10 +73,8 @@
// Opens a new filesystem and returns a name and root path for the requested
// filesystem and a success error code if the operation succeeds. If the
// operation fails, |error_code| indicates the reason for failure.
- // TODO(https://crbug.com/873661): Make interface per frame/worker and remove
- // |origin|.
[Sync]
- Open(url.mojom.Origin origin, blink.mojom.FileSystemType file_system_type) =>
+ Open(blink.mojom.FileSystemType file_system_type) =>
(string name,
url.mojom.Url root_url,
mojo_base.mojom.FileError error_code);
diff --git a/third_party/blink/renderer/core/fileapi/file_test.cc b/third_party/blink/renderer/core/fileapi/file_test.cc
index cef630ac..493a027 100644
--- a/third_party/blink/renderer/core/fileapi/file_test.cc
+++ b/third_party/blink/renderer/core/fileapi/file_test.cc
@@ -86,8 +86,7 @@
}
// mojom::blink::FileSystem
- void Open(const scoped_refptr<const SecurityOrigin>& origin,
- mojom::blink::FileSystemType file_system_type,
+ void Open(mojom::blink::FileSystemType file_system_type,
OpenCallback callback) override {}
void ResolveURL(const KURL& filesystem_url,
ResolveURLCallback callback) override {}
diff --git a/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.cc b/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.cc
index 2afbd35e..23e6075 100644
--- a/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.cc
+++ b/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.cc
@@ -114,23 +114,20 @@
}
void FileSystemDispatcher::OpenFileSystem(
- const SecurityOrigin* origin,
mojom::blink::FileSystemType type,
std::unique_ptr<FileSystemCallbacks> callbacks) {
GetFileSystemManager().Open(
- origin, type,
- blink::BindOnce(&FileSystemDispatcher::DidOpenFileSystem,
- WrapWeakPersistent(this), std::move(callbacks)));
+ type, blink::BindOnce(&FileSystemDispatcher::DidOpenFileSystem,
+ WrapWeakPersistent(this), std::move(callbacks)));
}
void FileSystemDispatcher::OpenFileSystemSync(
- const SecurityOrigin* origin,
mojom::blink::FileSystemType type,
std::unique_ptr<FileSystemCallbacks> callbacks) {
String name;
KURL root_url;
base::File::Error error_code = base::File::FILE_ERROR_FAILED;
- GetFileSystemManager().Open(origin, type, &name, &root_url, &error_code);
+ GetFileSystemManager().Open(type, &name, &root_url, &error_code);
DidOpenFileSystem(std::move(callbacks), std::move(name), root_url,
error_code);
}
diff --git a/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.h b/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.h
index af12f6b1..e754a9b3 100644
--- a/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.h
+++ b/third_party/blink/renderer/modules/filesystem/file_system_dispatcher.h
@@ -22,7 +22,6 @@
class Blob;
class ExecutionContext;
class KURL;
-class SecurityOrigin;
// Sends messages via mojo to the blink::mojom::FileSystemManager service
// running in the browser process. It is owned by ExecutionContext, and
@@ -43,11 +42,9 @@
mojom::blink::FileSystemManager& GetFileSystemManager();
- void OpenFileSystem(const SecurityOrigin* origin,
- mojom::blink::FileSystemType type,
+ void OpenFileSystem(mojom::blink::FileSystemType type,
std::unique_ptr<FileSystemCallbacks> callbacks);
- void OpenFileSystemSync(const SecurityOrigin* origin,
- mojom::blink::FileSystemType type,
+ void OpenFileSystemSync(mojom::blink::FileSystemType type,
std::unique_ptr<FileSystemCallbacks> callbacks);
void ResolveURL(const KURL& filesystem_url,
diff --git a/third_party/blink/renderer/modules/filesystem/local_file_system.cc b/third_party/blink/renderer/modules/filesystem/local_file_system.cc
index f73fcd0..1c60ea7 100644
--- a/third_party/blink/renderer/modules/filesystem/local_file_system.cc
+++ b/third_party/blink/renderer/modules/filesystem/local_file_system.cc
@@ -146,11 +146,9 @@
ExecutionContext* context = GetSupplementable();
FileSystemDispatcher& dispatcher = FileSystemDispatcher::From(context);
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/fileapi/file_test.cc b/third_party/blink/renderer/core/fileapi/file_test.cc
index cef630ac..493a027 100644
--- a/third_party/blink/renderer/core/fileapi/file_test.cc
+++ b/third_party/blink/renderer/core/fileapi/file_test.cc
@@ -86,8 +86,7 @@
}
// mojom::blink::FileSystem
- void Open(const scoped_refptr<const SecurityOrigin>& origin,
- mojom::blink::FileSystemType file_system_type,
+ void Open(mojom::blink::FileSystemType file_system_type,
OpenCallback callback) override {}
void ResolveURL(const KURL& filesystem_url,
ResolveURLCallback callback) override {}
Original Bug Report
FileSystem API access from PDF/sandboxed renderers via malformed opaque origin IPC
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can bypass ChildProcessSecurityPolicy restrictions by supplying a malformed opaque origin to FileSystemManager::Open. This hits an early-return path that skips is_pdf() and is_sandboxed() checks, granting the restricted process full read/write access to the hosting origin’s filesystem.
Affected files:
content/browser/file_system/file_system_manager_impl.cccontent/browser/child_process_security_policy_impl.ccthird_party/blink/public/mojom/filesystem/file_system.mojom
Estimated timestamp from git blame: 2026-02-03
Description
There is a potential defense-in-depth bypass in FileSystemManagerImpl::Open() that allows heavily restricted renderers (such as those hosting PDFs or sandboxed iframes) to access the FileSystem API for their hosting origin.
The vulnerability stems from inconsistent origin validation. FileSystemManagerImpl::Open() performs a security check (CanAccessDataForOrigin) using a renderer-supplied url::Origin parameter, but subsequently calls OpenFileSystem() using the browser-authoritative StorageKey securely captured from the Mojo receiver context.
An attacker in a compromised renderer can exploit this by sending an Open() IPC with a specially crafted, malformed opaque origin. Specifically, they can construct an opaque origin with an empty precursor tuple (i.e., scheme="", host="", port=0) but a valid nonce_if_opaque.
When ChildProcessSecurityPolicyImpl::CanAccessOrigin evaluates this decoy origin, the following occurs:
- The origin is parsed as opaque.
origin.GetTupleOrPrecursorTupleIfOpaque()returns the empty, invalid tuple.- The code hits an early-allow path (
content/browser/child_process_security_policy_impl.cc, line 2215) designed for opaque origins without precursors (e.g., browser-initiated data: URLs). - This path only verifies that the child process has a valid
SecurityState(which it does) and returnstrue.
Crucially, taking this early-return path bypasses the subsequent call to CanAccessMaybeOpaqueOrigin, skipping the rigorous actual_process_lock.is_sandboxed() and actual_process_lock.is_pdf() checks entirely.
Once the Open() check is bypassed, ContinueOpen proceeds to open the filesystem using the browser-authoritative StorageKey (the victim’s actual origin). Subsequent FileSystem operations (like Read/Write/Create) enforce permissions via CanCommitURL, which passes AccessType::kCanCommitNewOrigin to the policy checks. Because PDF processes and sandboxed frames are explicitly allowed to commit new origins (even if they are restricted from accessing their data), these subsequent checks pass, granting the attacker full access to the victim origin’s sandboxed filesystem.
Impact
This flaw allows process types that are architecturally intended to have zero storage access (like PDF renderers) to access the FileSystem API for the origin hosting them. While the binding’s StorageKey prevents cross-origin data access, this is a significant bypass of the restrictions placed on sandboxed and PDF processes.
Potential Steps to Reproduce
(Note: These are suggested steps based on code analysis; a working PoC has not been executed.)
- Compromise a renderer running in a process where
ProcessLock.is_sandboxed()orProcessLock.is_pdf()is true. - Obtain a
FileSystemManagerMojo binding via the frame’sBrowserInterfaceBroker. - Craft an
Open()IPC call with an opaque origin having an invalid precursor ({scheme:"", host:"", port:0, nonce_if_opaque:<any UnguessableToken>}). - The security check in
ChildProcessSecurityPolicyImplis bypassed due to the invalid precursor, andFileSystemManageropens the filesystem for the binding’s actualStorageKey. - Issue subsequent
Create,Write, orReadDirectoryIPCs using the returned root URL. These will succeed becauseCanCommitURLallows sandboxed/PDF processes to commit new origins.
Suggested Fix
The vestigial origin parameter should be removed from the FileSystemManager::Open Mojo interface entirely. The security check in FileSystemManagerImpl::Open should instead directly use the origin from the securely captured StorageKey (receivers_.current_context().origin()), ensuring validation is always performed against the browser-authoritative origin. (There is already an existing TODO in third_party/blink/public/mojom/filesystem/file_system.mojom tracking the removal of this parameter).
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.