Restrict ability for Network process to load files from temp directory
CVE: CVE-2026-43821 · Safari 26.6 · Released July 27, 2026 Impact: An app may be able to read files outside of its sandbox Apple's description: An access issue was addressed with improved access restrictions. Credit: Brian Carpenter
Medium — no memory corruption anywhere in this diff, just an authorization check that was skipped for an entire directory subtree. It reads as post-compromise file disclosure: a renderer that already fell can pull browser-staged data out of the temp directory without ever holding the token that was supposed to authorize the read.
Filesystem access inside WebKit's multi-process design is capability-based: a process that wants to read a path is expected to hold a sandbox extension, a token minted by a more-privileged process and handed over IPC, that names exactly that path. The Network process performs file:// reads on behalf of web content, so it is the process that has to decide whether an incoming load request carries the authorization it claims. NetworkResourceLoader::isLocalFileLoadAllowed() is where that decision lives on iOS — and it carried a compatibility carve-out that answered "yes" for one whole directory tree without asking for a token at all.
The angle: On iOS Safari, anything able to issue resource-load requests to the network process could read every file under the browser's container temp directory without ever presenting the token that was supposed to authorize the read.
Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
Patch Details
One functional hunk, inside the PLATFORM(IOS_FAMILY) block of NetworkResourceLoader::isLocalFileLoadAllowed(const URL&). The exemption's condition previously tested two things: that the container temporary directory path is non-empty, and that the canonicalized request path lies underneath it. The patch prepends a third conjunct, !WTF::IOSApplication::isMobileSafari(), so the early return true is no longer reachable when the host application is Safari on iOS. Supporting that predicate requires the new <wtf/cocoa/RuntimeApplicationChecksCocoa.h> include, added alongside the existing PathsBlockedForSandboxExtensions.h under PLATFORM(COCOA).
Everything else in the diff is comment text. The original note claimed the exemption existed for apps using the JS fetch API; the rewritten version broadens that to "the fetch JS API or -[WKWebView loadHTMLString:baseURL:]", and a new FIXME: rdar://177160334 block records the intended real fix — minting a sandbox extension for the base URL in WebPageProxy::loadDataWithNavigationShared — plus an explicit statement that the exemption stays for now because that change is larger. No tests accompany the commit.
The shape of the delta is worth stating plainly, because it is not "add a missing check":
Before: After:
isLocalFileLoadAllowed(url) isLocalFileLoadAllowed(url)
├─ path under container tmp? ├─ host app is MobileSafari?
│ └─► return true (no ext) │ └─► fall through to ext check
└─ else ──► require extension ├─ path under container tmp?
│ └─► return true (no ext)
└─ else ──► require extension
The permissive branch survives; it just acquires a client predicate in front of it.
Background
Process split. WebKit on iOS runs work across three process roles. The UI process has the app's full privileges and is the only one in direct contact with the user. WebContent processes render untrusted web content and are heavily sandboxed. The Network process performs loads on behalf of WebContent. Resource loads travel from WebContent to the Network process as IPC messages, and the Network process is expected to validate what it receives rather than take WebContent's assertions at face value — WebContent is the process a remote attacker gets to first.
Sandbox extensions. On iOS and macOS, a sandbox extension is a capability token: a more-privileged process (typically the UI process) mints it for a specific filesystem path and passes it over IPC, temporarily granting the receiving sandboxed process access to that path. Absent a matching extension, the sandbox profile denies the read outright. The extension is therefore the artifact that says "someone with authority intended this path to be reachable from here."
NetworkResourceLoader and the local-file decision. NetworkResourceLoader is the per-resource load object that receives a load request over IPC and drives it to completion. When the request is for a file:// URL, isLocalFileLoadAllowed() is the gate that decides whether the Network process may service it. The neighbouring PathsBlockedForSandboxExtensions.h encodes the inverse idea — a deny-list of paths for which extensions should never be honored.
Container temporary directory. Every iOS app gets a private container with a tmp/ subdirectory. NetworkProcess::containerTemporaryDirectory() returns the path of the hosting application's temp directory as the Network process sees it. For a third-party app embedding a web view, that directory holds files the app itself generated. For the system browser, it is a staging area for browser-managed data.
Path idioms in the check. FileSystem::realPath() canonicalizes a path, resolving symbolic links and .. components; FileSystem::isAncestor(dir, path) then tests whether the canonicalized path lies under dir. Together they are the standard "is this path inside that directory" idiom.
WTF::IOSApplication::isMobileSafari(). One of WTF's RuntimeApplicationChecks predicates, which report which application bundle is hosting WebKit. The family is conventionally used to scope compatibility quirks and behavior changes to particular apps.
-[WKWebView loadHTMLString:baseURL:]. A WKWebView API that loads an in-memory HTML string as though it had been served from baseURL. Subresource references inside that HTML resolve relative to the base URL, so a file:// base URL makes the loaded HTML pull local files.
Analysis
The root cause is a compatibility carve-out scoped by path when it needed to be scoped by client. Here is the authorization flow, with the exempted branch marked:
WebContent ──IPC: load file:///…/tmp/x ──► Network process
│
▼
isLocalFileLoadAllowed(url)
│
┌─────────────────────────┴──────────────────┐
│ path under containerTemporaryDirectory? │
└─────────────────────────┬──────────────────┘
yes │ │ no
▼ ▼
return true ◄── no token require sandbox
(exempt branch) extension
Both arrows out of that box lead to a filesystem read; only the right-hand one requires the requester to have been granted anything. The exemption existed for a real reason — a third-party app that writes a file into its own tmp/ and then fetches it from JS never had a UI-process navigation to mint an extension from, so WebContent has no token to forward and the load would simply fail. The carve-out papers over that by treating location as proof of permission.
The problem is the predicate the carve-out was keyed on. It tested the path and nothing else: not which application is hosting WebKit, not whether the load originated from a file-scheme document, not whether the UI process ever intended that path to be reachable. The missing invariant is that a compatibility exemption should extend only to the clients that actually need it. For a third-party embedder, "files in my own tmp directory" and "files my web view may read" are roughly the same set, and the trade is defensible. For the system browser they are not the same set at all — the container temp directory there is not scratch space some embedding app populated for its own web view, it is a staging area for browser-managed data, and every byte under it became loadable by anything that could get a resource-load IPC message to the Network process.
Who can send that message matters for calibrating the impact. A normal HTTP-origin document cannot fetch file:// URLs — same-origin and scheme policy in WebContent stops that well before IPC. The credible actor is a WebContent process that has already been compromised and can therefore issue load requests of its own construction. Such an attacker asks the Network process to read a path under the container temp directory; realPath() canonicalizes it, isAncestor() confirms containment, the function returns true, and the read happens in the Network process — whose sandbox is separate from, and in some respects broader than, the WebContent sandbox. The bytes come back as an ordinary resource-load response body.
That gives a bounded primitive: extension-free reads of files under the hosting application's container temp directory. There is no memory corruption, no write, no control-flow influence, and no new execution context — this is not a sandbox escape, and anything beyond reading those files still requires a separate one. Its practical role is the information-disclosure step after a WebContent compromise, and the payoff is capped by whatever the browser happened to have staged in that directory. That is what "an app may be able to read files outside of its sandbox" describes.
The fix restores the invariant by narrowing rather than removing. !WTF::IOSApplication::isMobileSafari() disqualifies the one client where the trade was least defensible, and the third-party compatibility path continues to work exactly as before. The FIXME: rdar://177160334 says so outright: the exemption stays for everyone else until WebPageProxy::loadDataWithNavigationShared learns to mint a real sandbox extension for the base URL.
A directory-containment test stood in for a capability token, so any process that could reach the loader could read the browser's temp directory without holding one.
Insight
The remediation is more interesting than the bug. Two things follow for anyone auditing WebKit after this patch. First, RuntimeApplicationChecks predicates are now load-bearing security logic rather than rendering quirks — a function that answers "is this Safari?" gates a filesystem authorization decision, which means WebKit's security posture varies by host application and the weaker posture is the default for everyone who is not Safari. Second, carve-outs like this get written once against one API and then silently inherited by every other path that reaches the same predicate. The comment edit in this very diff is the author discovering that -[WKWebView loadHTMLString:baseURL:] had been riding an exemption originally written for JS fetch.
Audit directions
-
Security predicates keyed on the hosting application rather than the request. Once an authorization decision is gated on a runtime app check, the codebase has as many security postures as it has recognized apps — and the unrecognized default is usually the permissive one. Narrow: grep
Source/WebKitforIOSApplication::andMacApplication::call sites and separate the cosmetic/layout quirks from those gating a filesystem, IPC, or extension decision;NetworkResourceLoader::isLocalFileLoadAllowedis now one of the latter, and each sibling deserves the question "what does the non-matching branch permit?". Wider: the same shape appears in any per-client policy relaxation that is not literally an app check — bundle-identifier allowlists, linked-on-or-after version checks (linkedOnOrAfter*), andWebPreferencescompatibility flags that disable a check for legacy embedders; the tell in code-search results is a boolean guard on client identity or SDK version sitting inside a function whose name containsisAllowed,shouldBlock,canAccess, orvalidate. Widest: the reusable invariant — a compatibility exemption must be scoped to the clients that need it, and the default branch must be the restrictive one — holds in Chromium's enterprise-policy and origin-trial overrides, in Android'stargetSdkVersion-gated permission behavior, and in any server-side feature flag that disables validation per tenant. Carry the question "which population gets the weak path by default, and how large is it?" into each. -
Directory-containment checks used as authorization, where membership in a path subtree substitutes for possession of a capability token. The danger is that the subtree's contents are controlled by someone other than the requester, so the check authorizes reads of files the requester never created. Narrow: audit every
FileSystem::isAncestorand path-prefix comparison inSource/WebKit/NetworkProcessandSource/WebKit/Shared, asking for each whether the directory is genuinely single-tenant with respect to the caller — start from thecontainerTemporaryDirectory()callers and fromPathsBlockedForSandboxExtensions, which encodes the inverse deny-list form of the same idea. Wider: the class covers any canonicalize-then-compare-then-open sequence — arealPathfollowed later by anopen()is a check-then-use window where a symlink or directory swap between the two steps would move the target outside the subtree, and deny-list variants fail differently from allow-list variants because a path not on the list is permitted; the tell is any function that resolves a path into a local and then passes the original string, or re-resolves, at the point of use. Widest: this is the general path-containment-as-authorization class, live in Node.js sandboxes built onpath.resolveprefix comparison, Go services usingfilepath.Cleanbefore a prefix test, and Python code usingos.path.commonprefix; the invariant to carry is containment proves location, never permission, and only at the instant it was evaluated. -
A receiving process tolerating the absence of an expected capability token. The bug is not the missing token but the code that has a fallback for it. Narrow: trace the callers of
NetworkResourceLoader::isLocalFileLoadAllowedand the surrounding file-scheme handling to enumerate every branch where a load proceeds without a sandbox extension, then check each for a compatibility rationale that has outlived its original API; the tell is a comment beginning "Some applications are relying on", or an unresolvedFIXME/radar reference sitting next to areturn true. Wider: the same shape lives anywhere WebKit accepts a resource identifier from a less-privileged process and reconstructs access without the token — blob and file-reference resolution (resolveBlobReferences), extension consumption in the GPU process, and any handler where a missing extension degrades to a permissive default instead of a hard failure; look for the asymmetry where the success path validates and the fallback path logs-and-allows. Widest: the invariant is when a capability token is missing, the only safe fallback is denial — it applies to Chromium's Mojo message handlers reconstructing browser-side state from renderer-supplied identifiers, to capability-based OS APIs (Fuchsia handles, Landlock, seccomp-scoped fds), and to any RPC layer with a "legacy client, no auth header" branch. The mental tell is a permission check whose else-branch is anything other than an error.