Remove ResourceRequest.m_cachePartition
CVE: CVE-2026-64753 · Safari 27 · Released September 14, 2026 Impact: Processing maliciously crafted web content may disclose sensitive user information Apple's description: A permissions issue was addressed by removing the vulnerable code. Credit: Viggo Lekdorf
Medium, and the ceiling is disclosure rather than corruption. Two fields that the loader requires to agree — the first party a load is attributed to, and the cache namespace it lands in — were separately settable, and four call sites each derived the second from a different source. Divergence means a lookup crosses a site boundary.
Web caches are shared state indexed by URL, which makes them a natural side channel between unrelated sites; every modern engine responds by widening the cache key to include the top-level site, so https://cdn.example/lib.js fetched under attacker.test occupies a different slot than the same URL fetched under bank.test. In WebKit that widening is a string — a registrable domain — carried on the request object itself, and both the in-process MemoryCache and the Network process's on-disk NetworkCache key on it. The invariant that makes the scheme sound is narrow and unstated in code: that string must always be the registrable domain of firstPartyForCookies, the field the request already carries to attribute cookies and same-site decisions.
The angle: Before the fix, a request could name one site as its first party while its cache lookups landed in a different site's partition — reading, or seeding, entries belonging to a top-level site the page never visited.
Source/WebCore/platform/network/ResourceRequestBase.cpp
Source/WebCore/platform/network/ResourceRequestBase.h
Source/WebCore/dom/ScriptExecutionContext.cpp
Source/WebCore/page/Page.cpp
Source/WebCore/platform/network/cocoa/ResourceRequestCocoa.mm
Source/WebKit/Shared/WebCoreArgumentCodersPlatform.serialization.in
Patch Details
The core of the change is a state-shape reduction: ResourceRequestBase loses its String m_cachePartition { emptyString() } member and gains a single bit, bool m_shouldBlockThirdPartyStorage : 1 { true }. cachePartition() stops being a stored-field accessor returning const String& and becomes a computed getter — clear bit returns emptyString(), otherwise it constructs RegistrableDomain domain(firstPartyForCookies()) and returns domain.string(). Because the value is now materialized per call, CachedResource::cachePartition() changes return type from const String& to String and drops its LIFETIME_BOUND annotation.
Everything that could write a partition independently is deleted: setCachePartition(const String&), the static partitionName(const String&) that wrapped PublicSuffixStore::singleton().topPrivatelyControlledDomain, and the setDomainForCachePartition helpers on ResourceRequestBase, CachedResourceRequest, and ScriptExecutionContext. The removal ripples outward in four clusters.
Call sites setting the bit. ThreadableWebSocketChannel::webSocketConnectRequest, DOMURL::revokeObjectURL, InspectorResourceUtilities::cachedResource, CachedResourceLoader::requestUserCSSStyleSheet and requestResource, XMLHttpRequest::createRequest, Internals::resourceFromMemoryCache, SWServer::createScriptRequest, and NetworkResourceLoader::continueWillSendRequest all swap a partition string for setShouldBlockThirdPartyStorage(...). LegacyWebArchive::createInternal gets a real behavioral addition — it now null-checks frame.document() and calls request.setFirstPartyForCookies(document->firstPartyForCookies()), which the derived getter requires and the old partition-only path never supplied.
Loader branches collapsing. DocumentLoader::loadMainResource drops its three-way branch — the subframe case that copied the main document's partition, and the main-frame case that built one from SecurityOrigin::create(mainResourceRequest.resourceRequest().url()) — and reduces to a single bool taken from the document or from settings().storageBlockingPolicy(). AsyncRevalidation and NetworkCacheSpeculativeLoadManager stop re-applying key.partition() onto revalidation requests; the speculative loader instead sets the bit from !key.partition().isEmpty().
Policy unification in ScriptExecutionContext. The m_domainForCachePartition override member is gone, along with the early return that consulted it ahead of the policy check. Page::setupForRemoteWorker no longer writes a partition string; it writes the policy itself via document->setStorageBlockingPolicy(document->settings().storageBlockingPolicy()).
IPC surface. WebCoreArgumentCodersPlatform.serialization.in now serializes bool shouldBlockThirdPartyStorage() in place of String cachePartition(), and WebCore::ServiceWorkerJobData carries bool shouldBlockThirdPartyStorage instead of String domainForCachePartition (constructor signature, isolatedCopy, and both ServiceWorkerContainer job-creation sites follow). The two IPC fuzzing layout tests swap cachePartition: '' for shouldBlockThirdPartyStorage: true accordingly.
On Cocoa, doUpdateResourceRequest stops importing an embedder-supplied partition entirely. The [NSURLProtocol propertyForKey:_kCFURLCachePartitionKey] read is wrapped in dynamic_objc_cast<NSString> and only its emptiness survives (m_shouldBlockThirdPartyStorage = !![cachePartition length]) — enough to detect the bit on requests CFNetwork hands back, such as redirects, which is what http/tests/cache/disk-cache/disk-cache-redirect.html exercises. _kCFURLCachePartitionKey remains the WebKit↔NSURLSession channel inside the Network process; what is gone is treating an app-supplied NSURLRequest's copy of it as authoritative.
Finally, the three cache-deletion helpers that depended on the removed static — MemoryCache::removeResourcesWithOrigin (both overloads) and NetworkProcess::deleteWebsiteDataForOrigin — recompute the partition through RegistrableDomain::uncheckedCreateFromHost(host) with an explicit isEmpty() ? emptyString() : string() fallback. The manually-cached-image CachedImage constructor taking a domainForCachePartition string is deleted outright.
Background
Cache partitioning. WebKit namespaces cached resources by a partition key so that a resource fetched while site A is the top-level page occupies a different cache slot than the same URL fetched under site B. The key is a string. In this diff it appears as the namespace argument to MemoryCache::removeResourcesWithOrigin(origin, partition), as NetworkCache::Key::partition(), and as the traversal filter in cache->traverse(cachePartition, ...).
Two caches, one key. MemoryCache is the in-WebContent resource cache that CachedResourceLoader and MemoryCache::resourceForRequest consult; NetworkCache is the Network process's on-disk HTTP cache. Both participate in ordinary page loads, and both key on the request's partition.
RegistrableDomain. WebCore's wrapper for the "top privately controlled domain" of a host — roughly public-suffix-plus-one (sub.example.co.uk → example.co.uk). PublicSuffixStore::singleton().topPrivatelyControlledDomain(host) is the underlying computation; RegistrableDomain::uncheckedCreateFromHost(host) builds one directly from a host string without re-parsing a URL.
firstPartyForCookies. A field on ResourceRequestBase holding the URL of the party a load is attributed to — for a subresource, the top-level document. Cookie policy and same-site determination already consult it.
StorageBlockingPolicy. A per-context enum (AllowAll, BlockThirdParty, …) expressing whether third-party storage access is permitted. ScriptExecutionContext stores it in m_storageBlockingPolicy, initialized to StorageBlockingPolicy::AllowAll in the constructor and normally overwritten from settings.
ResourceRequest across processes. A ResourceRequest is constructed in WebContent and serialized to the Network process. Its field list lives in Source/WebKit/Shared/WebCoreArgumentCodersPlatform.serialization.in, and the Cocoa variant is rebuilt on the receiving side by ResourceRequest::fromResourceRequestData. Any field declared there is influenceable to whatever degree the sending process is.
_kCFURLCachePartitionKey. A CFNetwork NSURLRequest property key used to convey a cache partition between WebKit and NSURLSession. [NSURLProtocol propertyForKey:inRequest:] returns an untyped id; dynamic_objc_cast<NSString> is WebKit's idiom for checking such a value's class before using it as a string.
Remote worker pages. Service workers run against a synthetic Page/Document assembled by Page::setupForRemoteWorker, which seeds that document with the worker's origin, privacy settings, and referrer policy directly rather than through a navigation. Separately, ServiceWorkerContainer::addRegistration/updateRegistration build a ServiceWorkerJobData in WebContent and send it to the SWServer, which converts it into a script-fetch ResourceRequest in SWServer::createScriptRequest.
Analysis
This is a denormalization bug: a security-relevant lookup key stored as independently mutable duplicate state instead of being derived from its source of truth.
Before: After:
ResourceRequest ResourceRequest
├─ m_firstPartyForCookies ──┐ ├─ m_firstPartyForCookies ──┐
└─ m_cachePartition │ └─ m_shouldBlockThirdParty │
▲ ▲ ▲ ▲ │ │ │
setCachePartition() (must │ cachePartition() ◄──────────┘
setDomainForCache…() agree) │ = RegistrableDomain(firstParty)
NSURLRequest property │
originalRequest() copy ──────┘ (no storage → nothing to desync)
The left column is the pre-fix shape. m_firstPartyForCookies and m_cachePartition are both request state, and the codebase requires them to agree — the partition must be the registrable domain of the first party. Nothing enforced that. setCachePartition() and setDomainForCachePartition() were public, so agreement was a per-call-site convention, and the deleted code shows the call sites did not honor it uniformly. DocumentLoader::loadMainResource derived the main-frame partition from SecurityOrigin::create(mainResourceRequest.resourceRequest().url()) — the request's own URL, not its first party. LegacyWebArchive::createInternal set a partition and no first party at all, which is why the fix has to add setFirstPartyForCookies there. NetworkResourceLoader::continueWillSendRequest deliberately copied the pre-redirect partition onto the post-redirect request. AsyncRevalidation and the speculative load manager re-applied key.partition() taken from a cache entry. Four sources, one field.
The one written-down statement of the invariant lived inside the setter the patch deletes:
void ResourceRequestBase::setCachePartition(const String& cachePartition)
{
ASSERT(!cachePartition.isNull());
ASSERT(cachePartition == partitionName(cachePartition));
m_cachePartition = cachePartition;
}
Both lines vanish in release builds — and this is a field that arrives over IPC. WebCoreArgumentCodersPlatform.serialization.in declared String cachePartition() on ResourceRequest, and ServiceWorkerJobData carried String domainForCachePartition for the registration path into SWServer::createScriptRequest. A debug assertion on deserialized data documents an invariant; it does not establish one. The same string had a third inbound path: doUpdateResourceRequest read _kCFURLCachePartitionKey off an embedder-supplied NSURLRequest and assigned the result — an untyped id, with no class check — straight into m_cachePartition.
ScriptExecutionContext carried a second copy of the same duplication, and this one has a concrete divergence visible in the diff:
setupForRemoteWorker (pre-fix):
m_storageBlockingPolicy = AllowAll ← constructor default, never written
m_domainForCachePartition = origin->domainForCachePartition()
│
domainForCachePartition() ────┘ early-returns the override,
never reaching the policy check
domainForCachePartition() returned m_domainForCachePartition whenever that override was non-null, short-circuiting the m_storageBlockingPolicy != StorageBlockingPolicy::BlockThirdParty test below it. Page::setupForRemoteWorker wrote only the override and left m_storageBlockingPolicy at the constructor's AllowAll. A remote-worker document therefore drew its partition from one field while its declared storage-blocking policy said something else — two authorities on third-party storage access, disagreeing by construction. The fix makes setupForRemoteWorker write the policy itself and deletes the override entirely, so the policy is the only authority left.
The consequence of divergence follows directly from what the partition is used for. It is the namespace component of both cache keys — MemoryCache::removeResourcesWithOrigin(origin, originPartition), NetworkCache::Key::partition(), cache->traverse(cachePartition, ...). When the stored partition names a registrable domain other than the one firstPartyForCookies designates, a store or a lookup lands in a different top-level site's partition than the load's actual first-party context, and the boundary that exists to keep site A from observing or populating site B's cached resources does not hold for that request. The primitives that follow are a cross-site cache read oracle — presence and timing of another site's cached resources, and for in-process MemoryCache hits potentially the cached response content — plus the ability to pre-seed a partition belonging to a site the user has not visited. This is user-data disclosure: browsing history and cached response bodies, matching the advisory's impact line. No memory-corruption primitive arises, and no sandbox boundary is crossed; the boundary at risk is a site boundary, and the IPC-string lever presupposes an already-compromised WebContent process. The diff establishes the divergence-capable state and two concrete shapes where it could arise — remote-worker setup, and a partition carried across a redirect — without a demonstrated end-to-end content-reachable divergence, so the rating rests on the bug class rather than a proven chain.
Post-fix, the right column of the diagram holds: the partition has no storage, so there is nothing to desynchronize. One structural detail deserves attention beyond the removal itself. The member default flipped direction. String m_cachePartition { emptyString() } meant a request whose call site forgot to set a partition landed in the shared empty partition — fail-open. bool m_shouldBlockThirdPartyStorage : 1 { true } paired with the derived getter means such a request is partitioned as soon as firstPartyForCookies is set, which the loader sets independently for cookie attribution — fail-closed.
That flip also relocates the attack surface. Where a request's partition was previously reachable through exactly two setters, it is now a pure function of firstPartyForCookies(), so every caller of setFirstPartyForCookies() now influences it — including requests mutated by an embedder delegate through updateFromDelegatePreservingOldProperties() and CFNetwork-generated requests such as redirects that flow through doUpdateResourceRequest. The new getter assumes firstPartyForCookies is itself authoritative and validated wherever it can be written; the commit message states that reasoning ("which should always have the same RegistrableDomain") rather than checking it at the getter. If a path exists that can set firstPartyForCookies to an unrelated site but previously could not touch the partition, cross-partition cache reads would follow from a primitive that formerly only mis-attributed cookies — which makes delegate-modified and redirect-derived requests the area worth auditing after this change.
A cache partition stored as a separately settable string — rather than derived from the request's first party — let a load's cache lookups land in a top-level site's namespace that the load did not belong to.
Insight
Cache partitioning is a security boundary implemented as a string key, and this commit is the structural fix for the whole class: stop storing the key, derive it from the principal. That is the direction other engines took for the same reason — Chromium replaced ad-hoc cache keys with a derived NetworkIsolationKey, Firefox derives partitioning from OriginAttributes — because any design where a derived key is also independently settable converts an invariant into a code-review obligation renewed at every call site, and this diff shows four call sites that each computed it from a different source: the request URL, the document's top origin, a cache entry's key, and the pre-redirect request.
Audit directions
-
A security-relevant key stored as an independently mutable copy of state it must always be derived from. Narrow: audit
ResourceRequestBase/RequestDatafor other members that mirror another member —isSameSite/isTopSiteversusfirstPartyForCookies, theOriginheader versus the requester origin, the inputs toisThirdParty(). The tell is a pair consisting of a derive-from helper (setXFromY(...)) alongside a rawsetX(...)callers can invoke on its own. Wider: the same class appears wherever a partition, quota, or isolation key is cached in a struct that also carries its source —NetworkCache::Key's partition versus the request's first party,ClientOriginalongside copies ofSecurityOriginData, storage-namespace keys inStorageNamespaceProvider— and in code-search results the shape is two fields whose agreement appears only inside anASSERT. Widest: the rule is if a field is a pure function of another field, expose it as a getter and never as storage, and it holds identically in Chromium'sNetworkIsolationKey, Firefox'sOriginAttributes, and any denormalized cache or index key in a database layer; the portable tell is a release build where the only thing keeping two fields consistent is a debug assertion. -
Normalization invariants a constructor enforces but a deserializer bypasses. Narrow: grep
Source/WebKit/Shared/*.serialization.inforStringand integer fields whose WebCore setter body has the shapeASSERT(x == canonicalize(x)); m_x = x;— the deletedResourceRequestBase::setCachePartitionwas exactly that, and the tell is a serialization entry with no[Validator=...]attribute feeding a setter whose only check is an assertion. Wider: extend to every cross-process field with a canonical form — registrable domains, origins andSecurityOriginData, MIME types, URLs,NetworkCache::Keycomponents — and check whether the receiving side re-derives or merely stores; the shape to look for is aCreateUsing=/fromXDatafactory assigning fields directly instead of routing them through validating setters. Widest: deserialization must re-establish every invariant the constructor established — the same failure shows up in protobuf and serde post-validation, JavareadObject, and any ORM hydrating objects past their constructors; the invariant to carry is that a value's provenance (constructed locally vs. reconstituted from a wire format) determines whether its class invariants still hold. -
Untyped external objects assigned into typed storage without a runtime class check. Narrow: grep
Source/WebCore/platform/network/cocoaandSource/WebKit/Shared/Cocoafor results of[NSURLProtocol propertyForKey:...],objectForKey:, andvalueForKey:assigned to aRetainPtr<NSString>/RetainPtr<NSNumber>/RetainPtr<NSDictionary>; the tell is an assignment with nodynamic_objc_cast<>and noisKindOfClass:guard — precisely the shapedoUpdateResourceRequesthad before this patch. Wider: any untyped container crossing a trust boundary into typed fields —NSUserDefaultsandCFPreferencesreads,NSKeyedUnarchiveroutput, XPC dictionaries, andNSURLRequestproperties set by the embedding app — where the search-result shape is anid-returning API whose value reaches aWTF::String, anint, or a C++ container in one step. Widest: a dynamically typed value entering a statically typed context must carry a receiver-side tag check, which applies to JavaObjectInputStream, Pythonpickle, and structured-clone data landing in native typed fields; the question to carry across codebases is "who validated the tag — the sender or me?", and only the second answer is safe. -
Per-request security state carried across a hop that changes the principal. Narrow: audit
NetworkResourceLoader::continueWillSendRequestand the redirect paths around it for fields deliberately re-applied from the pre-redirect request — the tell is a setter onnewRequestwhose argument comes fromoriginalRequest(), which is exactly how the pre-fix partition survived a cross-site redirect. Wider: everything re-applied or inherited after a redirect or document swap — cache policy,isSameSite/isTopSite, sandbox flags, CSP and referrer-policy inheritance, andupdateFromDelegatePreservingOldProperties, whose name states the pattern outright; the shape to notice is any "preserve old properties" helper invoked after the target URL has already changed. Widest: after a principal-changing hop, re-derive security state; never carry it forward — the same rule governs OAuthredirect_uriand token-audience validation, proxy and gateway hops that re-stamp identity headers, and session fixation across privilege changes; the question to carry is "which of these preserved fields encodes a principal, and did the principal just change?".