CVE-2026-113177
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/worker_host/worker_script_fetcher.cc |
modified | |
IN_PROC_BROWSER_TEST_Pcontent/browser/file_system/file_system_url_loader_factory_browsertest.cc |
modified |
Files Changed
content/browser/file_system/file_system_url_loader_factory.cccontent/browser/file_system/file_system_url_loader_factory_browsertest.cccontent/browser/file_system/file_system_url_loader_factory.hcontent/browser/worker_host/worker_script_fetcher.cc
Patch
From 292843a6fee913d3d0d80d2f6cc52c74e1640184 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@chromium.org>
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 <rakina@chromium.org>
Commit-Queue: Eriko Kurimoto <elkurin@chromium.org>
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;
Regression Test / PoC
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;
Original Bug Report
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:
- Starts a local server on port 8008.
- Loads a B top-level page at
http://localhost:8008/. - Embeds an ordinary A frame from
http://127.0.0.1:8008/. The A frame writesPDF_URL_LOADER_SECRET_9f8c2atopdf-secret.txtusingnavigator.storage.getDirectory(). - 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). - The renderer-only shim requests
filesystem:http://127.0.0.1:8008/temporary/pdf-secret.txtthrough 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.pypdf-renderer-url-loader-poc.patch
CREDIT INFORMATION
Reporter credit: Hoyong Jin