Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in SiteIsolation
DescriptionOut of bounds read in SiteIsolation
ComponentSiteIsolation
Sub-componentInternals>Plugins>PDF, Internals>Sandbox>SiteIsolation
Bug ClassOOB
Tracker549529981
Fix commit7a0ece0c748a (chromium/src) +88/-7
CISA KEVNot listed
Crediteda45hif
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
FileSystemURLLoaderFactoryTest
content/browser/file_system/file_system_url_loader_factory_browsertest.cc
modified
FileSystemURLSubresourceBrowserTest
content/browser/file_system/file_system_url_loader_factory_browsertest.cc
modified

Files Changed

  • content/browser/file_system/file_system_url_loader_factory_browsertest.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
From 7a0ece0c748a4065084d64d991e25b447d5522ce Mon Sep 17 00:00:00 2001
From: Alex Moshchuk <alexmos@chromium.org>
Date: Wed, 16 Sep 2026 22:27:54 -0700
Subject: [PATCH] Don't install FileSystemURLLoaderFactory for PDF documents

A PDF document commits a real origin, but its renderer process is
deliberately denied access to that origin's stored data
(ChildProcessSecurityPolicyImpl::IsAccessAllowedForPdfProcess() returns
false for kCanAccessDataForCommittedOrigin). As part of that, a PDF
document shouldn't be able to access filesystem: URLs.

This CL is a followup to https://crrev.com/c/8411305, which made
FileSystemEntryURLLoader::Start() check CanAccessDataForOrigin() instead
of CanCommitURL(), so a PDF document's filesystem: request is rejected
with net::ERR_INVALID_URL. It provides further hardening such that
RenderFrameHostImpl::CommitNavigation() no longer installs a
FileSystemURLLoaderFactory for PDF documents in the first place. Two
reasons:

- Defense in depth.  The guarantee no longer rests solely on the
  loader-side CPSP check.  A PDF document simply has no filesystem:
  factory to use.

- Cleaner failure mode.  Rather than reaching a browser-side loader
  that then rejects it, the request fails in the renderer as an
  unsupported scheme -- the same treatment content:// already receives
  a few lines above on Android (r1270300).

This is not an observable behavior change: from script, the request
fails either way. Accordingly, the added browser test
(FileSystemURLSubresourceBrowserTest.SubresourceLoadBlockedInPdfDocument)
passes with or without the CommitNavigation() change, and is added as
end-to-end regression coverage for the overall guarantee, and as the
only test that exercises the factory bundle documents actually receive
at commit time, rather than a factory constructed by the test.

Fixed: 549529981
Change-Id: I1366a3f733934ea9314cb3e35996cd7352abcb1f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8391596
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Commit-Queue: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Eriko Kurimoto <elkurin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699529}
---
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 61feedc..6e495d4 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -13326,13 +13326,18 @@
     }
 #endif
 
-    auto* partition = GetStoragePartition();
-    non_network_factories.emplace(
-        url::kFileSystemScheme,
-        CreateFileSystemURLLoaderFactory(
-            GetProcess()->GetDeprecatedID(), GetFrameTreeNodeId(),
-            partition->GetFileSystemContext(), partition->GetPartitionDomain(),
-            commit_params->storage_key));
+    // Only non-PDF documents can load filesystem: subresources.  PDF
+    // documents may commit an origin, but their processes are not allowed to
+    // access stored data for it.
+    if (!navigation_request->GetUrlInfo().embedder_isolation_info.is_pdf()) {
+      auto* partition = GetStoragePartition();
+      non_network_factories.emplace(
+          url::kFileSystemScheme,
+          CreateFileSystemURLLoaderFactory(
+              GetProcess()->GetID().GetUnsafeValue(), GetFrameTreeNodeId(),
+              partition->GetFileSystemContext(),
+              partition->GetPartitionDomain(), commit_params->storage_key));
+    }
 
     non_network_factories.emplace(url::kDataScheme,
                                   DataURLLoaderFactory::Create());
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 91dcf2b..4477056 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
@@ -36,15 +36,18 @@
 #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/renderer_host/render_frame_host_impl.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"
+#include "content/public/browser/navigation_controller.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
 #include "content/public/test/test_utils.h"
 #include "content/shell/browser/shell.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -181,6 +184,20 @@
   return line.find("<script>start(\"") == 0;
 }
 
+// Commits `url` in `web_contents` as PDF content, so that the resulting
+// document runs in a process whose SiteInfo has `is_pdf` set.
+RenderFrameHostImpl* NavigateToURLAsPdf(WebContents* web_contents,
+                                        const GURL& url) {
+  NavigationController::LoadURLParams params(url);
+  params.transition_type = ui::PageTransitionFromInt(
+      ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
+  params.is_pdf = true;
+  NavigateToURLBlockUntilNavigationsComplete(
+      web_contents, params, 1,
+      /*ignore_uncommitted_navigations=*/false);
+  return static_cast<RenderFrameHostImpl*>(web_contents->GetPrimaryMainFrame());
+}
+
 }  // namespace
 
 class FileSystemURLLoaderFactoryTest
@@ -1099,4 +1116,63 @@
           kValidExternalMountPoint));
 }
 
+// Exercises filesystem: subresource loads through the loader factory that
+// documents actually receive at commit time, rather than through a factory
+// created directly by the test.
+class FileSystemURLSubresourceBrowserTest : public ContentBrowserTest {
+ protected:
+  void SetUpOnMainThread() override {
+    ContentBrowserTest::SetUpOnMainThread();
+    ASSERT_TRUE(embedded_test_server()->Start());
+  }
+};
+
+// Verify that a document committed as PDF content cannot fetch filesystem:
+// subresources for its origin, while an ordinary document at the same origin
+// can.
+IN_PROC_BROWSER_TEST_F(FileSystemURLSubresourceBrowserTest,
+                       SubresourceLoadBlockedInPdfDocument) {
+  const GURL page_url = embedded_test_server()->GetURL("/title1.html");
+  EXPECT_TRUE(NavigateToURL(shell(), page_url));
+
+  // Create a temporary file from an ordinary document.
+  static constexpr char kWriteScript[] = R"(
+      new Promise((resolve, reject) => {
+        webkitRequestFileSystem(TEMPORARY, 1024, fs => {
+          fs.root.getFile('subresource.txt', {create: true}, entry => {
+            entry.createWriter(writer => {
+              writer.onwriteend = () => resolve('written');
+              writer.onerror = () => reject(writer.error);
+              writer.write(new Blob(['fs-data']));
+            }, reject);
+          }, reject);
+        }, reject);
+      });)";
+  ASSERT_EQ("written", EvalJs(shell(), kWriteScript));
+
+  static constexpr char kReadScript[] = R"(
+      new Promise(resolve => {
+        const request = new XMLHttpRequest();
+        request.open('GET', 'filesystem:' + location.origin +
+                                '/temporary/subresource.txt');
+        request.onload = () => resolve(request.responseText);
+        request.onerror = () => resolve('load-error');
+        request.send();
+      });)";
+
+  // The ordinary document can read the file back through its filesystem: URL.
+  ASSERT_EQ("fs-data", EvalJs(shell(), kReadScript));
+
+  // Commit a same-origin document as PDF content, so that it runs in a
+  // process whose SiteInfo has `is_pdf` set.
+  const GURL pdf_url = embedded_test_server()->GetURL("/title2.html");
+  RenderFrameHostImpl* frame =
+      NavigateToURLAsPdf(shell()->web_contents(), pdf_url);
+  ASSERT_EQ(pdf_url, shell()->web_contents()->GetLastCommittedURL());
+  ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
+
+  // The PDF document must not receive the file's contents.
+  EXPECT_EQ("load-error", EvalJs(frame, kReadScript));
+}
+
 }  // namespace content
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 91dcf2b..4477056 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
@@ -36,15 +36,18 @@
 #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/renderer_host/render_frame_host_impl.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"
+#include "content/public/browser/navigation_controller.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
 #include "content/public/test/test_utils.h"
 #include "content/shell/browser/shell.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -181,6 +184,20 @@
   return line.find("<script>start(\"") == 0;
 }
 
+// Commits `url` in `web_contents` as PDF content, so that the resulting
+// document runs in a process whose SiteInfo has `is_pdf` set.
+RenderFrameHostImpl* NavigateToURLAsPdf(WebContents* web_contents,
+                                        const GURL& url) {
+  NavigationController::LoadURLParams params(url);
+  params.transition_type = ui::PageTransitionFromInt(
+      ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
+  params.is_pdf = true;
+  NavigateToURLBlockUntilNavigationsComplete(
+      web_contents, params, 1,
+      /*ignore_uncommitted_navigations=*/false);
+  return static_cast<RenderFrameHostImpl*>(web_contents->GetPrimaryMainFrame());
+}
+
 }  // namespace
 
 class FileSystemURLLoaderFactoryTest
@@ -1099,4 +1116,63 @@
           kValidExternalMountPoint));
 }
 
+// Exercises filesystem: subresource loads through the loader factory that
+// documents actually receive at commit time, rather than through a factory
+// created directly by the test.
+class FileSystemURLSubresourceBrowserTest : public ContentBrowserTest {
+ protected:
+  void SetUpOnMainThread() override {
+    ContentBrowserTest::SetUpOnMainThread();
+    ASSERT_TRUE(embedded_test_server()->Start());
+  }
+};
+
+// Verify that a document committed as PDF content cannot fetch filesystem:
+// subresources for its origin, while an ordinary document at the same origin
+// can.
+IN_PROC_BROWSER_TEST_F(FileSystemURLSubresourceBrowserTest,
+                       SubresourceLoadBlockedInPdfDocument) {
+  const GURL page_url = embedded_test_server()->GetURL("/title1.html");
+  EXPECT_TRUE(NavigateToURL(shell(), page_url));
+
+  // Create a temporary file from an ordinary document.
+  static constexpr char kWriteScript[] = R"(
+      new Promise((resolve, reject) => {
+        webkitRequestFileSystem(TEMPORARY, 1024, fs => {
+          fs.root.getFile('subresource.txt', {create: true}, entry => {
+            entry.createWriter(writer => {
+              writer.onwriteend = () => resolve('written');
+              writer.onerror = () => reject(writer.error);
+              writer.write(new Blob(['fs-data']));
+            }, reject);
+          }, reject);
+        }, reject);
+      });)";
+  ASSERT_EQ("written", EvalJs(shell(), kWriteScript));
+
+  static constexpr char kReadScript[] = R"(
+      new Promise(resolve => {
+        const request = new XMLHttpRequest();
+        request.open('GET', 'filesystem:' + location.origin +
+                                '/temporary/subresource.txt');
+        request.onload = () => resolve(request.responseText);
+        request.onerror = () => resolve('load-error');
+        request.send();
+      });)";
+
+  // The ordinary document can read the file back through its filesystem: URL.
+  ASSERT_EQ("fs-data", EvalJs(shell(), kReadScript));
+
+  // Commit a same-origin document as PDF content, so that it runs in a
+  // process whose SiteInfo has `is_pdf` set.
+  const GURL pdf_url = embedded_test_server()->GetURL("/title2.html");
+  RenderFrameHostImpl* frame =
+      NavigateToURLAsPdf(shell()->web_contents(), pdf_url);
+  ASSERT_EQ(pdf_url, shell()->web_contents()->GetLastCommittedURL());
+  ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
+
+  // The PDF document must not receive the file's contents.
+  EXPECT_EQ("load-error", EvalJs(frame, kReadScript));
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by un...@gmail.com

Restricted PDF renderer can read matching-origin FileSystem data through the `filesystem:` URL loader


Report description

Restricted PDF renderer can read matching-origin FileSystem data through the filesystem: URL loader


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

Overview

A renderer process locked as a PDF process is intentionally permitted to commit the PDF document’s origin, but it is denied access to passwords, storage, and other data for that origin. The document subresource setup nevertheless gives PDF documents a filesystem: URL loader. That loader authorizes requests with CanCommitURL() instead of the stricter CanAccessDataForOrigin() decision.

With code execution in a restricted PDF renderer, an attacker can use this loader to read pre-existing legacy sandboxed FileSystem bytes belonging to the browser-supplied StorageKey. A process- and URL-guarded renderer patch reproduced the read twice from fresh profiles without DevTools, CDP, a browser harness, or unsafe/experimental flags.

Affected files

  • content/browser/renderer_host/render_frame_host_impl.cc
    • RenderFrameHostImpl::CommitNavigation() installs a FileSystemURLLoaderFactory for document subresources without applying the PDF data-access decision.
  • content/browser/file_system/file_system_url_loader_factory.cc
    • FileSystemEntryURLLoader::Start() checks only CanCommitURL() before cracking and serving the requested filesystem: URL.
  • content/browser/security/cpsp/child_process_security_policy_impl.cc
    • IsAccessAllowedForPdfProcess() permits commits but rejects kCanAccessDataForCommittedOrigin, defining the boundary bypassed here.

Tested versions

  • Live reproduction: Chromium 154.0.8012.0, revision 34a482b3cd98d6d599f43091babae366fb9c3d8e, macOS arm64.
  • Build configuration: is_debug=false, is_component_build=true, symbol_level=0; no security-relevant feature flags.
  • Reliability: 2 successful runs from 2 fresh disposable profiles.
  • Current published Mac Stable at validation time: Chrome 152.0.7977.54, Chromium revision 24072c1aa400ec4a89dc738b6b6acd12a8589b6f. The affected factory installation and CanCommitURL() check are present in that exact published source revision. The attached patch also applies there, but the recorded live runs used the revision above.

The only source modification used for the live reproduction is the attached renderer-only patch that models the stated compromised-PDF-renderer precondition. It is guarded by Chromium’s own --pdf-renderer process switch and by the exact loopback PDF URL. The browser-process implementation under test is unchanged.

Vulnerability details

During navigation commit, Chromium installs the factory with the authoritative process ID and browser-computed StorageKey:

non_network_factories.emplace(
    url::kFileSystemScheme,
    CreateFileSystemURLLoaderFactory(
        GetProcess()->GetDeprecatedID(), GetFrameTreeNodeId(),
        partition->GetFileSystemContext(), partition->GetPartitionDomain(),
        commit_params->storage_key));

The loader later validates the renderer using the weaker commit predicate:

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))) {
  net_error = net::ERR_INVALID_URL;
}

That predicate deliberately succeeds for a PDF process hosting a normal URL. The actual PDF storage policy is different:

case AccessType::kCanCommitNewOrigin:
case AccessType::kHostsOrigin:
  return true;
case AccessType::kCanAccessDataForCommittedOrigin:
  return false;

After the weak check, StartOnIOThread() calls:

url_ = params_.file_system_context->CrackURL(request.url,
                                             params_.storage_key);

FileSystemURL requires the URL origin to match the StorageKey origin, so this does not permit an arbitrary-origin key forgery. The issue is that the restricted PDF process is allowed to use its matching browser-derived key even though CanAccessDataForOrigin() rejects that process/origin pair.

The inconsistency is corroborated by Chromium change 42d042d4eacdf396f18e106a16b3f2045781ae37 (2026-07-30). That change replaced the same CanCommitURL() predicate with CanAccessDataForOrigin() in HasPermissionsForFileSystemFile() specifically because PDF processes may commit an origin but must not access its data. It did not update the URL-loader path.

Reproduction

  1. Use a clean checkout at 34a482b3cd98d6d599f43091babae366fb9c3d8e and apply the renderer-only compromised-process simulation:

    git apply --check /path/to/poc.patch
    git apply /path/to/poc.patch
    
  2. Build Chrome normally. For example:

    autoninja -C out/vrp-triage chrome
    
  3. Start the attached loopback server; it serves all HTML and JavaScript from /poc.html and writes the machine-readable result next to the script:

    python3 poc.py 8129
    
  4. Launch the patched build in a fresh disposable profile, without debugging, experimental, or unsafe flags, and open http://127.0.0.1:8129/poc.html.

  5. The final page displays PASS. The attached result contains:

    {
      "confirmed": true,
      "ordinary_frame_seeded_file": true,
      "pdf_renderer_read_exact_secret": true,
      "pdf_report": {
        "contentType": "text/html",
        "isTop": false,
        "localStorageIsNull": true,
        "ok": true,
        "rendererProcessGuard": "--pdf-renderer",
        "status": 200,
        "text": "PDF_FILESYSTEM_BOUNDARY_SECRET_8d71c2e4"
      }
    }
    

Actual result

The restricted PDF renderer received HTTP 200 and the exact synthetic bytes previously written by the ordinary Web frame. This reproduced in both fresh profile runs.

Expected result

Chromium should not install or service a filesystem: subresource loader for a renderer process that fails CanAccessDataForOrigin() for the factory’s browser-derived StorageKey origin. The PDF renderer’s request should fail without returning file bytes.

Attachment hashes

Attachment SHA-256
poc.py 5a05ed5d07fc89c03009bba623ef0253bf83a27ddcdb853a336098a340f89bb1
poc.patch b0c06f1d6dad9b9c0d4c5cbf59ebc48148f81011acfa9c1a0573a0dc12f3e402
test.pdf f56d93933ddf0d137e5711ce8c002bcda194e8874e1043d77a45aeba7edde79b
verified_result.json cb905a0dc3e458b32a17111b7cb2be1fc99c1c5ed07a7b7751d12cbb6cad3457

Impact analysis

Security impact

A compromised restricted PDF renderer can read legacy Temporary FileSystem files belonging to an ordinary Web frame when the PDF and frame resolve to the same origin and browser-assigned StorageKey. Depending on the application, such files may contain offline documents, exports, or cached private data.

The proof demonstrates confidentiality loss only:

  • an ordinary nested Web frame creates and verifies a synthetic Temporary FileSystem file;
  • Chromium loads a PDF with the matching partition topology;
  • the renderer patch runs only in the process carrying --pdf-renderer and only for that exact PDF URL;
  • the PDF leaf reports document.contentType === "text/html", window.localStorage === null, and self !== top, confirming the restricted document environment; and
  • an unchanged production XMLHttpRequest receives HTTP 200 and the exact pre-existing marker through the production FileSystemURLLoaderFactory.

The cause

What version of Chrome have you found the security issue in?

152.0.7977.54 [stable]

No, it is not related to a crash.

Choose the type of vulnerability

Site Isolation Bypass

How would you like to be publicly acknowledged for your report?

a45hif

View on issue tracker