7a0ece0c748a4065084d64d991e25b447d5522ce Don't install FileSystemURLLoaderFactory for PDF documents
Triage note: Defense-in-depth for the PDF-process data-access boundary; explicitly 'not an observable behavior change' (the loader-side CPSP check already blocked it). Fixed:549529981.
The bug at a glance
This is defense-in-depth hardening on top of an already-closed hole, not an exploitable bug in its own right. The prior CL (crrev.com/c/8411305) already made FileSystemEntryURLLoader::Start() enforce CanAccessDataForOrigin(), so a PDF document’s filesystem: request was already rejected with net::ERR_INVALID_URL before this change. This follow-up simply stops installing the FileSystemURLLoaderFactory for PDF documents at commit time, so the factory does not exist rather than existing-and-rejecting. The CL explicitly states this is not an observable behavior change and that the new browser test passes with or without the production code change. Given the guarantee was already enforced, the residual risk this closes is only ‘what if the loader-side CPSP check regresses or is bypassed’ – a real but second-order concern. CVSS 3.1 (low) fits: confidentiality-only, cross-origin/cross-process stored-data access, no integrity/availability impact, and no independent exploitability. It is isSecurity=true because the property being protected (PDF processes must not read their committed origin’s stored data) is a genuine site-isolation invariant, but the score is low precisely because the primitive was already denied.
The subtle point is that a PDF document commits a real origin – it is not opaque – yet its process is deliberately stripped of the ability to read that origin’s stored data (ChildProcessSecurityPolicyImpl::IsAccessAllowedForPdfProcess returns false for kCanAccessDataForCommittedOrigin). That creates an unusual gap between ‘can commit this URL’ and ‘can access data for this origin’. filesystem: URLs sit exactly in that gap: CanCommitURL would say yes (the origin matches), but CanAccessDataForOrigin says no. The interesting design lesson is that any per-document factory bundle keyed only on the committed origin, without also consulting the is_pdf carve-out, hands a PDF process a capability its process security policy says it must not have. This CL makes the factory installation itself origin-and-pdf-aware rather than relying on a downstream loader to re-check.
Root cause
During navigation commit, RenderFrameHostImpl::CommitNavigation assembles the bundle of non-network URLLoaderFactories the new document will receive (data:, filesystem:, and platform schemes). Previously it unconditionally added a FileSystemURLLoaderFactory keyed on the storage partition’s filesystem context and the commit’s storage_key. A PDF document therefore received a working filesystem: factory, and the only thing stopping it from reading its origin’s stored files was the loader-side CanAccessDataForOrigin check added in the predecessor CL. The change wraps the factory installation in a guard: if navigation_request->GetUrlInfo().embedder_isolation_info.is_pdf() is true, the filesystem: entry is simply never emplaced into non_network_factories. As a result a PDF document has no filesystem: loader at all, and a request for such a URL fails in the renderer as an unsupported scheme – the same clean failure content:// already gets on Android – instead of round-tripping to a browser-side loader that rejects it.
Key code
The FileSystemURLLoaderFactory is now installed only for non-PDF documents; a PDF document gets no filesystem: factory at all.
// 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));
}
Patch walkthrough
content/browser/renderer_host/render_frame_host_impl.cc— In CommitNavigation, wraps the previously unconditional emplace of the filesystem: factory in an if (!…is_pdf()) guard, reading the PDF flag from the navigation request’s UrlInfo embedder_isolation_info. When the document is a PDF, no filesystem: factory is added to the bundle. (The diff also shows the process-id accessor updated from GetDeprecatedID() to GetID().GetUnsafeValue(), an incidental modernization of the same call.) The data: factory emplace immediately below is unchanged, confirming only filesystem: is gated.content/browser/file_system/file_system_url_loader_factory_browsertest.cc— Adds a NavigateToURLAsPdf helper that commits a URL with LoadURLParams.is_pdf=true so the resulting document runs in a process whose SiteInfo has is_pdf set, and adds FileSystemURLSubresourceBrowserTest.SubresourceLoadBlockedInPdfDocument. That test writes a temporary file from an ordinary document, confirms the ordinary document can read it back via a filesystem: URL (fs-data), then commits a same-origin document as PDF content and asserts the PDF document gets load-error. The CL notes this test passes with or without the CommitNavigation change (because the loader-side check already blocks it) and is included as end-to-end regression coverage and as the only test exercising the real at-commit factory bundle rather than a test-constructed factory.
Background
PDF process data-access carve-out — ChildProcessSecurityPolicyImpl::IsAccessAllowedForPdfProcess() returns false for kCanAccessDataForCommittedOrigin: a PDF renderer commits a real origin but is denied read access to that origin’s stored data (filesystem, and more). This isolates the PDF renderer, which historically has been a rich attack surface, from the page’s persistent storage.
filesystem: URLs — The (now legacy) File System / sandboxed filesystem API exposes per-origin persistent and temporary storage addressable via filesystem:/temporary/… URLs. Reading one returns that origin’s stored bytes, which is exactly the stored-data access a PDF process must not have.
CanCommitURL vs CanAccessDataForOrigin — Two distinct CPSP questions. CanCommitURL asks whether a process may host a document at a URL; CanAccessDataForOrigin asks whether it may read that origin’s stored data. For PDF processes these diverge, and the predecessor CL (c/8411305) fixed FileSystemEntryURLLoader::Start to consult the latter instead of the former.
Non-network factory bundle at commit — At navigation commit the browser hands the renderer a curated map of URLLoaderFactories for non-network schemes. What is present in that bundle defines what the document can even attempt to load; omitting a scheme makes it fail as unsupported in the renderer without any browser round-trip.
Vulnerability window
- Predecessor fix — crrev.com/c/8411305 changed FileSystemEntryURLLoader::Start() to check CanAccessDataForOrigin() instead of CanCommitURL(), so a PDF document’s filesystem: request began failing with net::ERR_INVALID_URL. That closed the observable hole.
- This CL (hardening) — 2026-09-16, Alex Moshchuk removes the filesystem: factory from the commit bundle for PDF documents (Fixed: 549529981, Cr-Commit-Position 1699529), adding defense in depth and a cleaner renderer-side failure mode plus end-to-end regression coverage.
Proof of concept
No exploit exists here and none is warranted: the guarantee was already enforced by the predecessor CL, so there is nothing to demonstrate that would have worked before this change. The diff does contain a legitimate browser-test that shows the expected blocked behavior (a same-origin PDF document getting load-error when it tries to read a filesystem: URL that an ordinary document can read), reproduced below verbatim from the diff as the read/write scripts the test uses. This is regression coverage, not a bypass; there is no pre-patch state in which the PDF document actually obtained the data, per the CL’s own statement that the test passes with or without the production change.
Exploitation
- Hypothetical precondition — An attacker would need a document committed as PDF content that also carries a filesystem: factory for its origin – the state this CL removes. Post-predecessor-CL, even with the factory, the loader-side CanAccessDataForOrigin check rejects the request, so no data is returned.
- Residual risk closed — The only scenario this CL defends against is a future regression or bypass of the loader-side CPSP check: without the factory present at all, such a regression still cannot yield stored data to a PDF process because there is no filesystem: loader to invoke.
- No live primitive — There is no attacker-usable primitive introduced or left open; the change strictly reduces the PDF process’s reachable surface and produces an earlier, cleaner failure.
Detection & hunting
For defenders and detection engineers:
- filesystem: requests from is_pdf processes —
- net::ERR_INVALID_URL vs unsupported-scheme failures for filesystem: —
Audit directions
- Other non-network factories in the commit bundle vs the PDF carve-out —
- Other consumers of the CanCommitURL/CanAccessDataForOrigin divergence for PDF —
- embedder_isolation_info.is_pdf propagation —