CVE-2026-87505
Overview
Background
- `ChildProcessSecurityPolicyImpl`
- the browser-process component that decides which resources (origins, files, filesystems) a given renderer/child process is allowed to touch.
- `CanCommitURL()` vs `CanAccessDataForOrigin()`
- two authorization checks with different strictness: the former uses
AccessType::kCanCommitNewOrigin(may this process display/commit the origin) while the latter asks whether the process may actually access the data of that origin. - `FileSystemURL`
- a URL abstraction for the sandboxed/temporary filesystem API that carries an
origin()/StorageKeyidentifying whose data the file belongs to. - PDF process
- a locked, restricted process type that may commit an origin (to render its PDF) but is not permitted to read or write that origin’s stored data.
Root Cause Analysis
The changed code lives in HasPermissionsForFileSystemFile() in child_process_security_policy_impl.cc (function name taken from the commit subject, not the @@ header). Before the patch, the function gated its per-origin filesystem authorization on CanCommitURL(child_id, filesystem_url.origin().GetURL()), which internally uses AccessType::kCanCommitNewOrigin — a check that only asks whether the process is allowed to commit the origin, not whether it is allowed to access that origin’s data. Because a PDF process (and similar restricted processes) can legitimately commit an origin while being forbidden from touching its stored data, the weaker CanCommitURL() gate let such a process pass the origin check and reach the per-operation Can{Read,Write,Create,...}FileSystemFile decisions. The invariant violated is that FileSystem data-access should follow the same data-access policy that FileSystemManagerImpl::Open() already enforces, but this entry point used a laxer policy.
The fix switches the gate to CanAccessDataForOrigin(child_id, filesystem_url.origin()), so a process that may commit but not access an origin’s data is now correctly rejected; the removed comment/TODO confirm the old behavior was known-suspect (the in-tree TODO(lukasza) explicitly asked whether CanAccessDataForOrigin could be used here). This patch only tightens the check — it adds no new mechanism beyond substituting the stricter authorization predicate.
CanCommitURL / kCanCommitNewOrigin) instead of a data-access check; the fix replaces it with CanAccessDataForOrigin(), aligning the FileSystemURL entry points with the data-access policy the rest of the FileSystem stack enforces.Attack Path
- Obtain a restricted-but-committing process
An attacker drives the browser into a process that may commit an origin yet must not access its data, e.g. a PDF process committed to
https://foo.com(the regression testPdfProcessSandboxedFileSystemmodels exactly this viaEmbedderIsolationInfo::CreateForPdf()plusAddCommittedOrigin). - Issue a FileSystem operation for that committed origin
From that process, request a sandboxed/
kFileSystemTypeTemporaryFileSystemURLwhoseorigin()matches the committed origin, invoking one of theCan{Read,Write,Create,Delete,Copy,Move}FileSystemFileentry points that route throughHasPermissionsForFileSystemFile(). - Pass the too-weak origin gate
Pre-patch,
CanCommitURL()returns true for the committed origin, so the function does not early-returnfalseand instead proceeds to grant the per-operation permission, giving the process incorrect authorization to that origin’s sandboxed filesystem data. - Access data across the intended boundary (inferred)
The resulting read/write/create/delete of another security context’s sandboxed filesystem data is the incorrect-authorization primitive; any further escalation beyond cross-context FileSystem data access is
(inferred)and not demonstrated by the diff.
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pcontent/browser/security/cpsp/child_process_security_policy_unittest.cc |
modified |
Files Changed
content/browser/security/cpsp/child_process_security_policy_unittest.cccontent/browser/security/cpsp/child_process_security_policy_impl.cc
Audit Directions
- Commit-check used for data authorizationSearch for other call sites that gate data read/write on
CanCommitURL()orAccessType::kCanCommitNewOriginwhere aCanAccessDataForOrigin()data-access check is the correct predicate. - Divergent policy between entry pointsLook for storage/data APIs whose direct entry point (e.g.
FileSystemManagerImpl::Open()) enforces a stricter policy than a secondary authorization helper reachable for the same resource, indicating an inconsistent guard. - Restricted committing processesAudit code paths reachable from PDF, fenced, guest, or other locked processes that may commit an origin but must not touch its data, ensuring each data operation uses
CanAccessDataForOrigin()rather than a commit-oriented check.
Patch
From 42d042d4eacdf396f18e106a16b3f2045781ae37 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@google.com>
Date: Thu, 30 Jul 2026 11:27:09 -0700
Subject: [PATCH] Use CanAccessDataForOrigin in HasPermissionsForFileSystemFile
HasPermissionsForFileSystemFile() gated its origin check on
CanCommitURL(), which uses AccessType::kCanCommitNewOrigin and therefore
allows processes (such as PDF processes) that may commit an origin but
should not access its data. Switch to CanAccessDataForOrigin() so that
the per-operation Can{Read,Write,Create,...}FileSystemFile checks apply
the same data-access policy that FileSystemManagerImpl::Open() already
enforces. This also resolves the in-tree TODO.
Update FilePermissionGrantingAndRevoking to commit the test origin so
the stricter check passes, and add a regression test covering a
PDF-locked process and the sandboxed Temporary filesystem.
Bug: 517597701
Change-Id: I6d44550fe3a1b03e5044788f807461d0338f9b26
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8173685
Commit-Queue: Charlie Reis <creis@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671258}
---
diff --git a/content/browser/security/cpsp/child_process_security_policy_impl.cc b/content/browser/security/cpsp/child_process_security_policy_impl.cc
index 9de5bf2..9aeb6d5 100644
--- a/content/browser/security/cpsp/child_process_security_policy_impl.cc
+++ b/content/browser/security/cpsp/child_process_security_policy_impl.cc
@@ -2082,14 +2082,12 @@
child_id, filesystem_url.mount_filesystem_id(), permissions);
}
- // If |filesystem_url.origin()| is not committable in this process, then this
- // page should not be able to place content in that origin via the filesystem
- // API either.
- // TODO(lukasza): Audit whether CanAccessDataForOrigin can be used directly
- // here.
+ // If |filesystem_url.origin()| is not accessible in this process, then this
+ // page should not be able to access or place content in that origin via the
+ // filesystem API either.
// TODO(crbug.com/379869738) Remove GetUnsafeValue.
- if (!CanCommitURL(child_id.GetUnsafeValue(),
- filesystem_url.origin().GetURL())) {
+ if (!CanAccessDataForOrigin(child_id.GetUnsafeValue(),
+ filesystem_url.origin())) {
return false;
}
diff --git a/content/browser/security/cpsp/child_process_security_policy_unittest.cc b/content/browser/security/cpsp/child_process_security_policy_unittest.cc
index 11cec88..4f97a94 100644
--- a/content/browser/security/cpsp/child_process_security_policy_unittest.cc
+++ b/content/browser/security/cpsp/child_process_security_policy_unittest.cc
@@ -1007,6 +1007,7 @@
p->AddForTesting(kRendererProcess, browser_context());
LockProcessIfNeeded(kRendererProcess, browser_context(), GURL("http://foo/"));
+ p->AddCommittedOrigin(kRendererID, url::Origin::Create(GURL("http://foo/")));
base::FilePath file(TEST_PATH("/dir/testfile"));
file = file.NormalizePathSeparators();
@@ -1059,6 +1060,7 @@
p->AddForTesting(kRendererProcess, browser_context());
CheckHasNoFileSystemFilePermission(p, file, url);
LockProcessIfNeeded(kRendererProcess, browser_context(), GURL("http://foo/"));
+ p->AddCommittedOrigin(kRendererID, url::Origin::Create(GURL("http://foo/")));
CheckHasNoFileSystemFilePermission(p, file, url);
// Cleanup.
@@ -1842,6 +1844,63 @@
p->Remove(kRendererProcess);
}
+// Verify that a PDF process is denied access to the sandboxed filesystem of
+// the origin it has committed. This mirrors the data-access expectations in
+// PdfProcessEnforcements above for the FileSystemURL-based entry points.
+TEST_P(ChildProcessSecurityPolicyTest, PdfProcessSandboxedFileSystem) {
+ ChildProcessSecurityPolicyImpl* p =
+ ChildProcessSecurityPolicyImpl::GetInstance();
+
+ p->RegisterFileSystemPermissionPolicy(storage::kFileSystemTypeTemporary,
+ storage::FILE_PERMISSION_SANDBOX);
+
+ TestBrowserContext browser_context;
+ p->AddForTesting(kRendererProcess, &browser_context);
+
+ UrlInfo pdf_url_info(
+ UrlInfoInit(GURL("https://foo.com"))
+ .WithEmbedderIsolationInfo(EmbedderIsolationInfo::CreateForPdf()));
+ scoped_refptr<SiteInstanceImpl> pdf_instance =
+ SiteInstanceImpl::CreateForUrlInfo(&browser_context, pdf_url_info,
+ /*is_guest=*/false,
+ /*is_fenced=*/false,
+ /*is_fixed_storage_partition=*/false);
+ p->LockProcess(pdf_instance->GetIsolationContext(), kRendererProcess,
+ /*is_process_used=*/false,
+ ProcessLock::FromSiteInfo(pdf_instance->GetSiteInfo()));
+
+ auto foo_origin = url::Origin::Create(GURL("https://foo.com"));
+ p->AddCommittedOrigin(kRendererID, foo_origin);
+
+ base::FilePath file(TEST_PATH("/dir/testfile"));
+ file = file.NormalizePathSeparators();
+ storage::FileSystemURL url = storage::FileSystemURL::CreateForTest(
+ blink::StorageKey::CreateFirstParty(foo_origin),
+ storage::kFileSystemTypeTemporary, file);
+
+ // A PDF process should not be able to access data for any origin, including
+ // an origin that it has committed, so all sandboxed filesystem operations
+ // for that origin should be rejected.
+ EXPECT_FALSE(p->CanReadFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanWriteFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanCreateFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanCreateReadWriteFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanCopyIntoFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanDeleteFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanMoveFileSystemFile(kRendererProcess, url, url));
+ EXPECT_FALSE(p->CanCopyFileSystemFile(kRendererProcess, url, url));
+
+ auto handle = p->CreateHandle(kRendererProcess);
+ EXPECT_FALSE(handle.CanReadFileSystemFile(url));
+ EXPECT_FALSE(handle.CanWriteFileSystemFile(url));
+ EXPECT_FALSE(handle.CanCreateFileSystemFile(url));
+ EXPECT_FALSE(handle.CanDeleteFileSystemFile(url));
+ EXPECT_FALSE(handle.CanMoveFileSystemFile(url, url));
+ EXPECT_FALSE(handle.CanCopyFileSystemFile(url, url));
+
+ p->Remove(kRendererProcess);
+}
+
// Test the granting of origin permissions, and their interactions with
// granting scheme permissions.
TEST_P(ChildProcessSecurityPolicyTest, OriginGranting) {
Regression Test / PoC
diff --git a/content/browser/security/cpsp/child_process_security_policy_unittest.cc b/content/browser/security/cpsp/child_process_security_policy_unittest.cc
index 11cec88..4f97a94 100644
--- a/content/browser/security/cpsp/child_process_security_policy_unittest.cc
+++ b/content/browser/security/cpsp/child_process_security_policy_unittest.cc
@@ -1007,6 +1007,7 @@
p->AddForTesting(kRendererProcess, browser_context());
LockProcessIfNeeded(kRendererProcess, browser_context(), GURL("http://foo/"));
+ p->AddCommittedOrigin(kRendererID, url::Origin::Create(GURL("http://foo/")));
base::FilePath file(TEST_PATH("/dir/testfile"));
file = file.NormalizePathSeparators();
@@ -1059,6 +1060,7 @@
p->AddForTesting(kRendererProcess, browser_context());
CheckHasNoFileSystemFilePermission(p, file, url);
LockProcessIfNeeded(kRendererProcess, browser_context(), GURL("http://foo/"));
+ p->AddCommittedOrigin(kRendererID, url::Origin::Create(GURL("http://foo/")));
CheckHasNoFileSystemFilePermission(p, file, url);
// Cleanup.
@@ -1842,6 +1844,63 @@
p->Remove(kRendererProcess);
}
+// Verify that a PDF process is denied access to the sandboxed filesystem of
+// the origin it has committed. This mirrors the data-access expectations in
+// PdfProcessEnforcements above for the FileSystemURL-based entry points.
+TEST_P(ChildProcessSecurityPolicyTest, PdfProcessSandboxedFileSystem) {
+ ChildProcessSecurityPolicyImpl* p =
+ ChildProcessSecurityPolicyImpl::GetInstance();
+
+ p->RegisterFileSystemPermissionPolicy(storage::kFileSystemTypeTemporary,
+ storage::FILE_PERMISSION_SANDBOX);
+
+ TestBrowserContext browser_context;
+ p->AddForTesting(kRendererProcess, &browser_context);
+
+ UrlInfo pdf_url_info(
+ UrlInfoInit(GURL("https://foo.com"))
+ .WithEmbedderIsolationInfo(EmbedderIsolationInfo::CreateForPdf()));
+ scoped_refptr<SiteInstanceImpl> pdf_instance =
+ SiteInstanceImpl::CreateForUrlInfo(&browser_context, pdf_url_info,
+ /*is_guest=*/false,
+ /*is_fenced=*/false,
+ /*is_fixed_storage_partition=*/false);
+ p->LockProcess(pdf_instance->GetIsolationContext(), kRendererProcess,
+ /*is_process_used=*/false,
+ ProcessLock::FromSiteInfo(pdf_instance->GetSiteInfo()));
+
+ auto foo_origin = url::Origin::Create(GURL("https://foo.com"));
+ p->AddCommittedOrigin(kRendererID, foo_origin);
+
+ base::FilePath file(TEST_PATH("/dir/testfile"));
+ file = file.NormalizePathSeparators();
+ storage::FileSystemURL url = storage::FileSystemURL::CreateForTest(
+ blink::StorageKey::CreateFirstParty(foo_origin),
+ storage::kFileSystemTypeTemporary, file);
+
+ // A PDF process should not be able to access data for any origin, including
+ // an origin that it has committed, so all sandboxed filesystem operations
+ // for that origin should be rejected.
+ EXPECT_FALSE(p->CanReadFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanWriteFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanCreateFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanCreateReadWriteFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanCopyIntoFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanDeleteFileSystemFile(kRendererProcess, url));
+ EXPECT_FALSE(p->CanMoveFileSystemFile(kRendererProcess, url, url));
+ EXPECT_FALSE(p->CanCopyFileSystemFile(kRendererProcess, url, url));
+
+ auto handle = p->CreateHandle(kRendererProcess);
+ EXPECT_FALSE(handle.CanReadFileSystemFile(url));
+ EXPECT_FALSE(handle.CanWriteFileSystemFile(url));
+ EXPECT_FALSE(handle.CanCreateFileSystemFile(url));
+ EXPECT_FALSE(handle.CanDeleteFileSystemFile(url));
+ EXPECT_FALSE(handle.CanMoveFileSystemFile(url, url));
+ EXPECT_FALSE(handle.CanCopyFileSystemFile(url, url));
+
+ p->Remove(kRendererProcess);
+}
+
// Test the granting of origin permissions, and their interactions with
// granting scheme permissions.
TEST_P(ChildProcessSecurityPolicyTest, OriginGranting) {
Original Bug Report
PDF Sandbox Bypass via FileSystemManagerImpl Sibling Methods
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: A compromised PDF renderer process can potentially bypass Site Isolation data restrictions and read, write, create, or delete files in its serving origin’s sandboxed HTML5 FileSystem. While FileSystemManagerImpl::Open() correctly validates access using CanAccessDataForOrigin, its sibling methods utilize CanCommitURL, which inadvertently permits PDF processes to access partitioned storage. This allows a compromised PDF renderer to access same-origin temporary or persistent storage without calling Open().
Affected files:
content/browser/child_process_security_policy_impl.cccontent/browser/file_system/file_system_manager_impl.cc
Estimated timestamp from git blame: 2024-06-26
Description
A potential security boundary bypass exists in the blink.mojom.FileSystemManager IPC validation layer where a compromised PDF renderer process can access, modify, or delete files within the HTML5 sandboxed FileSystem (Temporary and Persistent storage) of its serving origin.
Normally, PDF renderer processes are isolated from accessing storage, passwords, or other sensitive user data of their hosting origins. This restriction is enforced by ChildProcessSecurityPolicyImpl::IsAccessAllowedForPdfProcess which returns false for AccessType::kCanAccessDataForCommittedOrigin checks.
While FileSystemManagerImpl::Open() correctly validates access via CanAccessDataForOrigin (and thus properly blocks PDF processes), its 13 sibling methods (including Create, Write, Remove, Move, Copy, etc.) bypass Open() and perform authorization checks that resolve to HasPermissionsForFileSystemFile. This function gates access on CanCommitURL instead of CanAccessDataForOrigin. Because PDF processes must be allowed to commit hosting origins for normal frame behavior, CanCommitURL permits the check, resulting in a full storage policy bypass.
Potential Attack Path
Because our analysis is performed statically without a live execution environment, the following steps represent a potential attack scenario:
- A user opens a webpage that hosts or embeds an attacker-controlled PDF served from
https://example.com/document.pdf. - The browser allocates a dedicated PDF renderer process for the PDF frame, locking the process to
https://example.comwith PDF-specific restrictions enabled. - The attacker exploits a vulnerability (e.g., in PDFium or V8) inside the PDF renderer to achieve remote code execution (RCE).
- The compromised renderer binds the
blink.mojom.FileSystemManagerinterface viaBrowserInterfaceBroker. - Instead of calling
Open(), the compromised renderer directly invokes any of the interface’s sibling methods, such asCreate(path="filesystem:https://example.com/temporary/malicious_file", ...)orWrite(...). - On the browser side,
FileSystemManagerImpl::Create()parses the URL and invokesChildProcessSecurityPolicyImpl::CanCreateFileSystemFile. HasPermissionsForFileSystemFileperforms aCanCommitURLcheck on the origin. Because the PDF process is authorized to commithttps://example.comURLs (AccessType::kCanCommitNewOriginevaluates totrue), the check succeeds.- Since temporary and persistent filesystem types possess
FILE_PERMISSION_SANDBOXpermissions, the request is unconditionally approved, allowing the compromised PDF renderer full read/write access tohttps://example.com’s sandboxed filesystem.
Code References
In content/browser/child_process_security_policy_impl.cc (lines 1965–1971):
// If |filesystem_url.origin()| is not committable in this process, then this
// page should not be able to place content in that origin via the filesystem
// API either.
// TODO(lukasza): Audit whether CanAccessDataForOrigin can be used directly
// here.
if (!CanCommitURL(child_id.GetUnsafeValue(),
filesystem_url.origin().GetURL())) {
return false;
}
In content/browser/child_process_security_policy_impl.cc (lines 2284–2288):
switch (access_type) {
case AccessType::kCanCommitNewOrigin:
case AccessType::kHostsOrigin:
return true;
case AccessType::kCanAccessDataForCommittedOrigin:
return false;
}
Suggested Fix
Update ChildProcessSecurityPolicyImpl::HasPermissionsForFileSystemFile to use CanAccessDataForOrigin rather than CanCommitURL to perform the security check, as suggested by the in-tree TODO comment. This ensures that PDF processes (and other processes with data-access restrictions) are correctly prevented from accessing storage via the file system sibling IPC methods.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.