← All reports

Restrict ability for Network process to load files from temp directory

MediumWebKit Network process —AuthBypass

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

Severity: Medium | Component: WebKit Network process — NetworkResourceLoader | 74d0c62 | Bugzilla 314867

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

#if PLATFORM(COCOA)
#include "PathsBlockedForSandboxExtensions.h"
+#include <wtf/cocoa/RuntimeApplicationChecksCocoa.h>
#endif

Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp

bool NetworkResourceLoader::isLocalFileLoadAllowed(const URL& url)
{
#if PLATFORM(IOS_FAMILY)
- // Some applications are relying on using the fetch JS API to load local files they have created in their temp directory.
+ // Some 3rd party apps are relying on using the fetch JS API or -[WKWebView loadHTMLString:baseURL:] to load local files in their temp directory.
// In this case, the WebContent process will not provide the Networking process with a sandbox extension to that file, since it does not have access.
// This is because the load is not initiated from the UI process which would provide an extension, but from JS in the WebContent process.
// To continue supporting this undocumented feature, we should allow local file loads from that location.
 
+ // FIXME: rdar://177160334
+ // The method -[WKWebView loadHTMLString:baseURL:] can be used to load local files by referring to links relative to the base URL in the HTML string.
+ // When the app is using -[WKWebView loadHTMLString:baseURL:] to load files in the temp directory, we should create a sandbox extension for the base URL.
+ // This can be done in WebPageProxy::loadDataWithNavigationShared. However, this is a larger change, so for now we rely on this exemption.
+
String directory = connectionToWebProcess().networkProcess().containerTemporaryDirectory();
- if (!directory.isEmpty() && FileSystem::isAncestor(directory, FileSystem::realPath(url.fileSystemPath()))) {
+ if (!WTF::IOSApplication::isMobileSafari() && !directory.isEmpty() && FileSystem::isAncestor(directory, FileSystem::realPath(url.fileSystemPath()))) {
RELEASE_LOG(Network, "shouldAllowLocalFileLoad: allowing loads from the temp directory");
return true;
}

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.

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.

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.

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.