Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in FileSystem
DescriptionUse after free in FileSystem
ComponentFileSystem
Sub-componentBlink>Storage>FileSystem, Internals>Sandbox>SiteIsolation
Bug ClassUAF
Tracker559795243
Fix commit292843a6fee9 (chromium/src) +71/-14
CISA KEVNot listed
CreditedHoyong Jin
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
content/browser/worker_host/worker_script_fetcher.cc
modified
IN_PROC_BROWSER_TEST_P
content/browser/file_system/file_system_url_loader_factory_browsertest.cc
modified

Files Changed

  • content/browser/file_system/file_system_url_loader_factory.cc
  • content/browser/file_system/file_system_url_loader_factory_browsertest.cc
  • content/browser/file_system/file_system_url_loader_factory.h
  • content/browser/worker_host/worker_script_fetcher.cc
From 292843a6fee913d3d0d80d2f6cc52c74e1640184 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <[email protected]>
Date: Tue, 15 Sep 2026 23:54:02 -0700
Subject: [PATCH] Check CanAccessDataForOrigin in FileSystemEntryURLLoader::Start

FileSystemEntryURLLoader::Start previously checked only CanCommitURL()
for the requesting renderer process. PDF renderer processes are allowed
to commit the PDF document's origin (CanCommitURL returns true), but
must not access storage/filesystem data for that origin
(CanAccessDataForOrigin returns false).

Additionally, WorkerScriptFetcher::CreateFactoryBundle was passing
ChildProcessHost::kInvalidUniqueID when creating the filesystem: URL
loader factory for LoaderType::kMainResource, which would bypass process
checks when a worker main script is loaded from a filesystem: URL.

This CL:
- Updates FileSystemEntryURLLoader::Start() to verify both
  CanCommitURL() and CanAccessDataForOrigin().
- Passes worker_process_id to CreateFileSystemURLLoaderFactory() for
  all loader types in WorkerScriptFetcher::CreateFactoryBundle().
- Adds a browser test (FileSystemURLLoaderFactoryTest.PdfProcessFileBlocked)
  verifying that PDF renderer processes are blocked from loading
  filesystem: URLs.

TAG=agy
CONV=cab82d07-89ff-4a22-ae29-675ff1ca806a

Bug: 559795243
Change-Id: I7f8e4fef49d27db3ba022907c298120a0eb503f2
Fixed: 559795243
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8411305
Reviewed-by: Rakina Zata Amni <[email protected]>
Commit-Queue: Eriko Kurimoto <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1698687}
---
diff --git a/content/browser/file_system/file_system_url_loader_factory.cc b/content/browser/file_system/file_system_url_loader_factory.cc
index 82d7d7e..eaefe84 100644
--- a/content/browser/file_system/file_system_url_loader_factory.cc
+++ b/content/browser/file_system/file_system_url_loader_factory.cc
@@ -141,13 +141,16 @@
     // necessary to avoid "no security state" failures when querying
     // ChildProcessSecurityPolicy.
     //
-    // If the requested URL is not committable in the current process, block the
-    // request.  This prevents one origin from fetching filesystem: resources
-    // belonging to another origin, see https://crbug.com/964245.
+    // If the current process is not allowed to access data for the storage
+    // key's origin, block the request. This prevents one origin from fetching
+    // filesystem: resources belonging to another origin (see
+    // https://crbug.com/964245), and prevents processes such as PDF renderer
+    // processes (which may commit an origin but must not access its storage
+    // data) from reading filesystem: resources.
     if (params_.render_process_host_id != ChildProcessHost::kInvalidUniqueID &&
         (!RenderProcessHost::FromID(params_.render_process_host_id) ||
-         !ChildProcessSecurityPolicyImpl::GetInstance()->CanCommitURL(
-             params_.render_process_host_id, request.url))) {
+         !ChildProcessSecurityPolicyImpl::GetInstance()->CanAccessDataForOrigin(
+             params_.render_process_host_id, params_.storage_key.origin()))) {
       DVLOG(1) << "Denied unauthorized request for "
                << request.url.possibly_invalid_spec();
       net_error = net::ERR_INVALID_URL;
diff --git a/content/browser/file_system/file_system_url_loader_factory.h b/content/browser/file_system/file_system_url_loader_factory.h
index b28d4a7..427ce9c8 100644
--- a/content/browser/file_system/file_system_url_loader_factory.h
+++ b/content/browser/file_system/file_system_url_loader_factory.h
@@ -35,11 +35,9 @@
 //   ChildProcessHost::kInvalidUniqueID (there is no process yet).
 // - For a factory created to pass to the renderer for subresource requests from
 //   the frame: that renderer process's ID.
-// - For a factory created for a browser-initiated worker main script request:
-//   ChildProcessHost::kInvalidUniqueID (permissions are checked later via
-//   CanCommitURL in PlzWorker).
-// - For a factory created to pass to the renderer for subresource requests from
-//   the worker: that renderer process's ID.
+// - For a factory created for a worker main script request or to pass to the
+//   renderer for subresource requests from the worker: that renderer process's
+//   ID.
 //
 // `frame_tree_node_id` is the ID of the FrameTreeNode where the requests are
 // associated.
diff --git a/content/browser/worker_host/worker_script_fetcher.cc b/content/browser/worker_host/worker_script_fetcher.cc
index f1e8e787..5da1ab585 100644
--- a/content/browser/worker_host/worker_script_fetcher.cc
+++ b/content/browser/worker_host/worker_script_fetcher.cc
@@ -621,13 +621,10 @@
   non_network_factories.emplace(url::kDataScheme,
                                 DataURLLoaderFactory::Create());
   if (filesystem_url_support) {
-    int process_id = loader_type == LoaderType::kMainResource
-                         ? ChildProcessHost::kInvalidUniqueID
-                         : worker_process_id;
     non_network_factories.emplace(
         url::kFileSystemScheme,
         CreateFileSystemURLLoaderFactory(
-            process_id, FrameTreeNodeId(),
+            worker_process_id, FrameTreeNodeId(),
             storage_partition->GetFileSystemContext(), storage_domain,
             request_initiator_storage_key));
   }
diff --git a/content/browser/file_system/file_system_url_loader_factory_browsertest.cc b/content/browser/file_system/file_system_url_loader_factory_browsertest.cc
index 9951e35..91dcf2b 100644
--- a/content/browser/file_system/file_system_url_loader_factory_browsertest.cc
+++ b/content/browser/file_system/file_system_url_loader_factory_browsertest.cc
@@ -35,6 +35,10 @@
 #include "build/build_config.h"
 #include "components/file_access/scoped_file_access.h"
 #include "components/file_access/test/mock_scoped_file_access_delegate.h"
+#include "content/browser/process_lock.h"
+#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
+#include "content/browser/site_instance_impl.h"
+#include "content/browser/url_info.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
@@ -231,6 +235,20 @@
             base::BindOnce(&FileSystemURLLoaderFactoryTest::OnOpenFileSystem,
                            loop.QuitClosure())));
     loop.Run();
+
+    ChildProcessSecurityPolicyImpl::GetInstance()->AddCommittedOrigin(
+        render_frame_host()->GetProcess()->GetID().GetUnsafeValue(),
+        url::Origin::Create(GURL("http://remote/")));
+    ChildProcessSecurityPolicyImpl::GetInstance()->AddCommittedOrigin(
+        render_frame_host()->GetProcess()->GetID().GetUnsafeValue(),
+        url::Origin::Create(GURL("http://automount/")));
+    ChildProcessSecurityPolicyImpl::GetInstance()->AddCommittedOrigin(
+        render_frame_host()->GetProcess()->GetID().GetUnsafeValue(),
+        url::Origin::Create(GURL("http://noauto/")));
+    // Mark the process as used so that subsequent cross-origin navigations
+    // (e.g., in CrossOriginFileBlocked) do not reuse this process while
+    // retaining the committed origins added above.
+    render_frame_host()->GetProcess()->SetIsUsed();
   }
 
   void TearDownOnMainThread() override {
@@ -499,10 +517,12 @@
     return context;
   }
 
+ protected:
   RenderFrameHost* render_frame_host() const {
     return shell()->web_contents()->GetPrimaryMainFrame();
   }
 
+ private:
   std::unique_ptr<network::TestURLLoaderClient> TestLoadHelper(
       const GURL& url,
       const std::optional<url::Origin>& origin,
@@ -792,6 +812,45 @@
   EXPECT_EQ(net::ERR_INVALID_URL, client->completion_status().error_code);
 }
 
+// Verify that a PDF renderer process is denied access to filesystem: URLs even
+// for an origin that it has committed.
+IN_PROC_BROWSER_TEST_P(FileSystemURLLoaderFactoryTest, PdfProcessFileBlocked) {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  WriteFile(
+      "file1.dat",
+      base::as_byte_span(kTestFileData).first(std::size(kTestFileData) - 1));
+
+  ChildProcessSecurityPolicyImpl* policy =
+      ChildProcessSecurityPolicyImpl::GetInstance();
+  ChildProcessId process_id = render_frame_host()->GetProcess()->GetID();
+
+  UrlInfo pdf_url_info(
+      UrlInfoInit(GURL("http://remote/"))
+          .WithEmbedderIsolationInfo(EmbedderIsolationInfo::CreateForPdf()));
+  scoped_refptr<SiteInstanceImpl> pdf_instance =
+      SiteInstanceImpl::CreateForUrlInfo(
+          shell()->web_contents()->GetBrowserContext(), pdf_url_info,
+          /*is_guest=*/false,
+          /*is_fenced=*/false,
+          /*is_fixed_storage_partition=*/false);
+  policy->LockProcess(pdf_instance->GetIsolationContext(), process_id,
+                      /*is_process_used=*/false,
+                      ProcessLock::FromSiteInfo(pdf_instance->GetSiteInfo()));
+
+  // Although the PDF process can commit the http://remote/ origin, it must not
+  // be allowed to access its filesystem data.
+  EXPECT_TRUE(policy->CanCommitURL(process_id.GetUnsafeValue(),
+                                   CreateFileSystemURL("file1.dat")));
+  EXPECT_FALSE(policy->CanAccessDataForOrigin(
+      process_id.GetUnsafeValue(),
+      url::Origin::Create(GURL("http://remote/"))));
+
+  auto client = TestLoad(CreateFileSystemURL("file1.dat"));
+  EXPECT_FALSE(client->has_received_response());
+  ASSERT_TRUE(client->has_received_completion());
+  EXPECT_EQ(net::ERR_INVALID_URL, client->completion_status().error_code);
+}
+
 IN_PROC_BROWSER_TEST_P(FileSystemURLLoaderFactoryTest,
                        FileTestFullSpecifiedRange) {
   base::ScopedAllowBlockingForTesting allow_blocking;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/file_system/file_system_url_loader_factory_browsertest.cc b/content/browser/file_system/file_system_url_loader_factory_browsertest.cc
index 9951e35..91dcf2b 100644
--- a/content/browser/file_system/file_system_url_loader_factory_browsertest.cc
+++ b/content/browser/file_system/file_system_url_loader_factory_browsertest.cc
@@ -35,6 +35,10 @@
 #include "build/build_config.h"
 #include "components/file_access/scoped_file_access.h"
 #include "components/file_access/test/mock_scoped_file_access_delegate.h"
+#include "content/browser/process_lock.h"
+#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
+#include "content/browser/site_instance_impl.h"
+#include "content/browser/url_info.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
@@ -231,6 +235,20 @@
             base::BindOnce(&FileSystemURLLoaderFactoryTest::OnOpenFileSystem,
                            loop.QuitClosure())));
     loop.Run();
+
+    ChildProcessSecurityPolicyImpl::GetInstance()->AddCommittedOrigin(
+        render_frame_host()->GetProcess()->GetID().GetUnsafeValue(),
+        url::Origin::Create(GURL("http://remote/")));
+    ChildProcessSecurityPolicyImpl::GetInstance()->AddCommittedOrigin(
+        render_frame_host()->GetProcess()->GetID().GetUnsafeValue(),
+        url::Origin::Create(GURL("http://automount/")));
+    ChildProcessSecurityPolicyImpl::GetInstance()->AddCommittedOrigin(
+        render_frame_host()->GetProcess()->GetID().GetUnsafeValue(),
+        url::Origin::Create(GURL("http://noauto/")));
+    // Mark the process as used so that subsequent cross-origin navigations
+    // (e.g., in CrossOriginFileBlocked) do not reuse this process while
+    // retaining the committed origins added above.
+    render_frame_host()->GetProcess()->SetIsUsed();
   }
 
   void TearDownOnMainThread() override {
@@ -499,10 +517,12 @@
     return context;
   }
 
+ protected:
   RenderFrameHost* render_frame_host() const {
     return shell()->web_contents()->GetPrimaryMainFrame();
   }
 
+ private:
   std::unique_ptr<network::TestURLLoaderClient> TestLoadHelper(
       const GURL& url,
       const std::optional<url::Origin>& origin,
@@ -792,6 +812,45 @@
   EXPECT_EQ(net::ERR_INVALID_URL, client->completion_status().error_code);
 }
 
+// Verify that a PDF renderer process is denied access to filesystem: URLs even
+// for an origin that it has committed.
+IN_PROC_BROWSER_TEST_P(FileSystemURLLoaderFactoryTest, PdfProcessFileBlocked) {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  WriteFile(
+      "file1.dat",
+      base::as_byte_span(kTestFileData).first(std::size(kTestFileData) - 1));
+
+  ChildProcessSecurityPolicyImpl* policy =
+      ChildProcessSecurityPolicyImpl::GetInstance();
+  ChildProcessId process_id = render_frame_host()->GetProcess()->GetID();
+
+  UrlInfo pdf_url_info(
+      UrlInfoInit(GURL("http://remote/"))
+          .WithEmbedderIsolationInfo(EmbedderIsolationInfo::CreateForPdf()));
+  scoped_refptr<SiteInstanceImpl> pdf_instance =
+      SiteInstanceImpl::CreateForUrlInfo(
+          shell()->web_contents()->GetBrowserContext(), pdf_url_info,
+          /*is_guest=*/false,
+          /*is_fenced=*/false,
+          /*is_fixed_storage_partition=*/false);
+  policy->LockProcess(pdf_instance->GetIsolationContext(), process_id,
+                      /*is_process_used=*/false,
+                      ProcessLock::FromSiteInfo(pdf_instance->GetSiteInfo()));
+
+  // Although the PDF process can commit the http://remote/ origin, it must not
+  // be allowed to access its filesystem data.
+  EXPECT_TRUE(policy->CanCommitURL(process_id.GetUnsafeValue(),
+                                   CreateFileSystemURL("file1.dat")));
+  EXPECT_FALSE(policy->CanAccessDataForOrigin(
+      process_id.GetUnsafeValue(),
+      url::Origin::Create(GURL("http://remote/"))));
+
+  auto client = TestLoad(CreateFileSystemURL("file1.dat"));
+  EXPECT_FALSE(client->has_received_response());
+  ASSERT_TRUE(client->has_received_completion());
+  EXPECT_EQ(net::ERR_INVALID_URL, client->completion_status().error_code);
+}
+
 IN_PROC_BROWSER_TEST_P(FileSystemURLLoaderFactoryTest,
                        FileTestFullSpecifiedRange) {
   base::ScopedAllowBlockingForTesting allow_blocking;
Loading diff…

Original Bug Report

reported by [email protected]

PDF renderer can read origin OPFS data through the `filesystem:` URL loader (missed consumer of issue 517597701)

VULNERABILITY DETAILS

FileSystemEntryURLLoader::Start() in content/browser/file_system/file_system_url_loader_factory.cc authorizes a renderer request for a filesystem: URL with ChildProcessSecurityPolicyImpl::CanCommitURL() only.

This check is insufficient for a PDF-only renderer process. Chromium’s PDF process policy intentionally allows the process to commit and host the PDF document’s origin, while CanAccessDataForOrigin() returns false because the process must not access that origin’s storage data.

After the CanCommitURL() check succeeds, the browser calls FileSystemContext::CrackURL() with the factory’s captured StorageKey and then directly reaches GetMetadata(), ReadDirectory(), and CreateFileStreamReader(). This path does not pass through the corrected CanReadFileSystemFile() family of checks or through FileSystemManagerImpl::Open().

As a result, code executing in a sandboxed PDF renderer can use the scheme-specific factory already installed in its frame’s URL loader bundle to read files from the target origin’s sandboxed filesystem/OPFS partition. The URL is deterministic:

filesystem:<origin>/temporary/<path>

A trailing-slash request also returns a directory listing, allowing filenames to be enumerated before individual files are read.

This is a missed consumer of the authorization rule fixed for issue 517597701. Chromium CL 8173685 changed HasPermissionsForFileSystemFile() from CanCommitURL() to CanAccessDataForOrigin() specifically because PDF processes may commit an origin but must not access its data. The separate URL-loader implementation retains the weaker check.

VERSION

Chrome Version: Chromium 154.0.8037.0 (Beta), source commit e967b7961d80199276ce493904c95f5fbbc42fca

Operating System: Ubuntu 26.04 LTS, Linux 7.0.0-31-generic, x86_64

I also checked Chromium main at 241a0c0c825e512dde450b3ee7bb2a562ab1ed98 on 2026-09-11. The URL-loader file still used CanCommitURL() and contained no CanAccessDataForOrigin() check.

REPRODUCTION CASE

Please attach poc.py and pdf-renderer-url-loader-poc.patch directly.

The renderer patch is a small test shim that models code execution in a PDF renderer. It does not change browser-process authorization. When a real PDF frame commits, it constructs the deterministic filesystem: URL and invokes the frame’s existing URL loader bundle.

From the Chromium source directory at the commit above:

git apply --check /path/to/pdf-renderer-url-loader-poc.patch
git apply /path/to/pdf-renderer-url-loader-poc.patch
gn gen out/vrp --args='is_debug=false is_component_build=true is_official_build=false symbol_level=0 blink_symbol_level=0 v8_symbol_level=0 use_remoteexec=false'
autoninja -C out/vrp chrome
python3 /path/to/poc.py ./out/vrp/chrome

The PoC performs the following steps automatically:

  1. Starts a local server on port 8008.
  2. Loads a B top-level page at http://localhost:8008/.
  3. Embeds an ordinary A frame from http://127.0.0.1:8008/. The A frame writes PDF_URL_LOADER_SECRET_9f8c2a to pdf-secret.txt using navigator.storage.getDirectory().
  4. Embeds a real A-origin PDF under the same B top-level page. The ordinary A frame and the A PDF OOPIF have the same partitioned StorageKey (A/^0B).
  5. The renderer-only shim requests filesystem:http://127.0.0.1:8008/temporary/pdf-secret.txt through the PDF frame’s loader bundle and prints the returned body.

The script launches Chrome with a fresh profile and without --no-sandbox. Observed output:

[PDF_FS_URL_LOADER_POC] PDF_URL_LOADER_SECRET_9f8c2a
[PASS] sandboxed PDF renderer received OPFS bytes: PDF_URL_LOADER_SECRET_9f8c2a
[SANDBOX] pid=625680 NoNewPrivs=1 Seccomp=2

The test helper keeps response bytes that have already been delivered because this special non-network loader leaves its decoded completion length at zero. This only affects SimpleURLLoader bookkeeping. A normal XHR control in the same actual PDF OOPIF returned HTTP 200 and the same complete body.

The relevant StorageKeys were also measured directly in the actual frame tree:

ordinary A seed iframe: http://127.0.0.1:8008/^0http://localhost
A PDF OOPIF:            http://127.0.0.1:8008/^0http://localhost

IMPACT

An attacker with code execution in the PDF renderer can read sandboxed filesystem/OPFS data belonging to the PDF origin in the matching StorageKey partition. A cross-origin embedding page cannot read the same A-origin data through normal web APIs because of the origin boundary, but the compromised PDF renderer receives it from the browser process.

The demonstrated impact is file-body disclosure. The loader implementation also supports directory listing and metadata disclosure. Access to other StorageKey partitions is not demonstrated.

ROOT CAUSE

The vulnerable condition is:

!ChildProcessSecurityPolicyImpl::GetInstance()->CanCommitURL(
    params_.render_process_host_id, request.url)

For a live PDF process and a URL matching its document origin, this returns false for the negated condition and permits the read. The authorization must also enforce the data-access decision for the captured StorageKey origin, for example:

!policy->CanCommitURL(process_id, request.url) ||
!policy->CanAccessDataForOrigin(process_id,
                                params_.storage_key.origin())

CrackURL() checking that the URL origin matches the captured StorageKey does not enforce the PDF data-access restriction; it only keeps the request within that factory’s partition.

FIXED CONTROL

I tested the same renderer shim and PoC after adding only the missing browser-process CanAccessDataForOrigin() check from suggested-fix.patch:

git apply /path/to/suggested-fix.patch
autoninja -C out/vrp chrome
python3 /path/to/poc.py ./out/vrp/chrome

The PDF was requested, but the secret was not returned:

[PDF_FS_URL_LOADER_POC] <LOAD_FAILED>
[FAIL] secret was not returned through the PDF filesystem: loader.
PDF requested: True

The non-zero result from poc.py is the expected fixed behavior.

FILES TO ATTACH

Attach directly, without an archive:

  • poc.py
  • pdf-renderer-url-loader-poc.patch

CREDIT INFORMATION

Reporter credit: Hoyong Jin

View on issue tracker