[2] Validate connection access to DOMCache with DOMCacheIdentifier
Another site's cache will happily serve back what you wrote into it.
High — the identical confused-deputy shape as the FileSystem fix, one storage type over. A renderer compromise limited to one site reaches every site's cached HTTP responses, and the write direction persists: a poisoned entry is served back as same-origin content on the victim's next visit.
The Cache Storage API (caches.open(), Cache.match/put/delete) keeps request/response pairs on behalf of each site, with the actual records held by the network process and every operation crossing IPC. A cache is named across that boundary by a DOMCacheIdentifier, minted when the cache is opened and quoted back on subsequent messages. The trust arrangement is the same as for OPFS: WebProcess-to-NetworkProcess IPC is untrusted input, and the network process is expected to confirm that the objects it resolves belong to the sender's site.
The angle: a compromised WebProcess can name another site's cache by identifier and read its stored HTTP responses, delete them, or write attacker-authored entries that the victim site later serves back to itself.
Many CacheStorage-related messages sent to NetworkStorageManager only carry a DOMCacheIdentifier when asking to operate on DOMCache storage, and NetworkStorageManager does not check whether the sender process actually has access to the requested cache. This lets a compromised process forge a DOMCacheIdentifier and access data from other origins. The fix stores the origin in CacheStorageManager and adds an origin accessor to CacheStorageCache, so NetworkStorageManager can run isSiteAllowedForConnection in the CacheStorage message handlers.
Source/WebKit/NetworkProcess/storage/NetworkStorageManager.cpp
Source/WebKit/NetworkProcess/storage/CacheStorageCache.cpp
Patch Details
CacheStorageManager now stores the owning origin unconditionally as a WebCore::ClientOrigin m_origin member and exposes origin() const. The previous std::optional<WebCore::ClientOrigin> constructor parameter — which existed only to decide whether to write the origin marker file — is replaced by a plain const WebCore::ClientOrigin& plus a new enum class ShouldWriteOriginFile : bool { No, Yes }. OriginStorageManager::StorageBucket::cacheStorageManager correspondingly passes the origin always and folds the m_level < UnifiedOriginStorageLevel::Standard test into the new ShouldWriteOriginFile argument, so the origin is retained even at Standard level where it was previously dropped.
CacheStorageCache::origin() const is added, forwarding through the WeakPtr<CacheStorageManager> m_manager and returning std::nullopt if the manager is gone.
Six message handlers — cacheStorageRemoveCache, cacheStorageReference, cacheStorageDereference, cacheStorageRetrieveRecords, cacheStorageRemoveRecords, cacheStoragePutRecords — gain a MESSAGE_CHECK/MESSAGE_CHECK_COMPLETION of origin && isSiteAllowedForConnection(connection.uniqueID(), RegistrableDomain { origin->topOrigin }) immediately after the m_cacheStorageRegistry->cache(cacheIdentifier) lookup. cacheStorageRemoveCache, cacheStorageRetrieveRecords and cacheStorageRemoveRecords had no IPC::Connection& parameter at all before this patch and had their signatures widened in NetworkStorageManager.h.
Treating an opaque identifier as a capability — an IPC handler resolves a caller-supplied ID through a globally-scoped registry without re-checking that the caller owns the resolved object.
Background
Where this lives.
NetworkStorageManager is the network-process-side entry point for storage IPC; the CacheStorage stack under it is CacheStorageRegistry, CacheStorageManager, CacheStorageCache and OriginStorageManager. It sits on the WebContent↔Network process trust boundary and is one of the enforcement points for per-site storage isolation.
Cache Storage API.
caches.open(name) returns a Cache object whose records — request/response pairs — are stored by the network process on disk or in memory. Cache.match, Cache.put and Cache.delete are each a separate IPC round trip to NetworkStorageManager.
Origin keys.
WebCore::ClientOrigin is a pair: topOrigin (the top-level document's origin, i.e. the storage partition) and clientOrigin (the origin of the frame or worker actually using the storage). Storage in modern WebKit is keyed by this pair, not a single origin. WebCore::RegistrableDomain is the eTLD+1 "site" derived from an origin; site-level granularity is what process-per-site isolation and the storage site-validation checks operate on.
DOMCacheIdentifier and the registry.
DOMCacheIdentifier is an ObjectIdentifier minted in the network process when a cache is opened and handed back to the WebProcess; CacheStorageCache derives from Identified<WebCore::DOMCacheIdentifier>, so the identifier is the cache's identity for all subsequent messages. CacheStorageRegistry is a per-session table (HashMap<DOMCacheIdentifier, WeakPtr<CacheStorageCache>>) resolving identifier to live cache. It spans every origin in the session — origin partitioning lives one level up, in OriginStorageManager/CacheStorageManager, not in the registry.
isSiteAllowedForConnection.
NetworkStorageManager's predicate for whether a given WebProcess connection may touch storage for a given site; it is the enforcement point for the storageSiteValidationEnabled mode configured on NetworkStorageManager.
MESSAGE_CHECK / MESSAGE_CHECK_COMPLETION.
WebKit's IPC validation macros. On failure they treat the message as malformed and tear down the sending connection; the _COMPLETION variant additionally runs a supplied completion handler first, required for handlers that owe the sender a reply.
UnifiedOriginStorageLevel.
A configuration knob controlling how much of a session's storage lives under the unified per-origin directory layout. At levels below Standard, per-feature directories still carry their own origin marker file describing which origin owns them.
Threat model. WebProcess-to-NetworkProcess IPC is untrusted input. A renderer that has been taken over can send any message with any field values, so every handler must validate that the objects it resolves belong to the sender.
Analysis
The handlers that operate on an already-opened cache carried only the DOMCacheIdentifier in the message — cacheStorageRetrieveRecords, cacheStorageRemoveRecords and cacheStorageRemoveCache did not even receive the IPC::Connection& — and resolved the identifier straight through the registry into a CacheStorageCache with no check that the sending WebProcess was entitled to the origin that owns it. The origin-scoped handlers in the same file (cacheStorageOpenCache, cacheStorageAllCaches, lockCacheStorage, unlockCacheStorage) take a ClientOrigin from the message and route through originStorageManager(origin, ...), so they are subject to the existing site-validation path; the identifier-keyed handlers bypassed it entirely.
A second, enabling defect sat one layer down. OriginStorageManager::StorageBucket::cacheStorageManager passed the origin as a std::optional populated only when m_level < UnifiedOriginStorageLevel::Standard, because the sole consumer was StorageUtilities::writeOriginToFile. At Standard level the CacheStorageManager therefore had no idea which origin it belonged to, so no authorization check was even expressible from a DOMCacheIdentifier.
Before: After:
cacheStorageRetrieveRecords(id) cacheStorageRetrieveRecords(conn, id)
└─► registry->cache(id) └─► registry->cache(id)
└─► cache->retrieveRecords() └─► cache->origin()
(any origin's records) └─► isSiteAllowedForConnection?
├─ no ─► MESSAGE_CHECK fails,
│ connection terminated
└─ yes ─► retrieveRecords()
The vulnerable handlers are IPC message receivers, not web-content-reachable primitives — ordinary JavaScript only ever obtains DOMCacheIdentifier values for its own caches through cacheStorageOpenCache, which is origin-checked. The realistic attacker is a WebProcess that already has arbitrary-IPC capability, which is exactly the model isSiteAllowedForConnection exists to defend.
Tracing the pre-fix code, the read direction runs in three steps. First, the attacker's process obtains a DOMCacheIdentifier naming a victim site's live CacheStorageCache — the identifier space is a single flat namespace in CacheStorageRegistry::m_caches, so enumeration across a modest integer range would suffice if identifiers are counter-derived, and no rate limiting or lockout exists on failed lookups (a miss simply returns Error::Internal via the if (!cache) early return, leaving the connection alive). Second, it sends CacheStorageRetrieveRecords with that identifier and a RetrieveRecordsOptions whose request.url() is null — CacheStorageCache::findRecords takes the url.isNull() branch and returns every record in the cache, not just a matching one. Third, cache->retrieveRecords(...) runs to completion and the callback ships the victim's CrossThreadRecord vector, responseBody included, back over the attacker's connection.
The write direction is the mirror image: CacheStoragePutRecords with the victim's identifier and attacker-authored CrossThreadRecord entries. The only remaining validation in that handler before the patch was the record.responseBodySize >= computeRealBodySizeForStorage(record.responseBody) size check — nothing tying the record's request URL or the cache to the sender's site. If the victim site later calls caches.match() on a poisoned key, the network process would return the attacker's Response under the victim's origin, which could yield script execution in the victim's context should the poisoned entry be a script or document the victim site loads from cache. CacheStorageRemoveCache and CacheStorageRemoveRecords give the destructive variant; CacheStorageReference/CacheStorageDereference let a foreign connection perturb the victim cache's m_cacheRefConnections bookkeeping and, plausibly, drive removeUnusedCache on a cache it does not own.
The vulnerable handlers execute in the NetworkProcess on NetworkStorageManager's work queue; the attacker sits in a WebContent process. This is not a sandbox escape — no code execution in the network process is implied. What it defeats is the isolation the multi-process split is supposed to buy. Post-fix, the same attempt terminates the offending connection through MESSAGE_CHECK.
This vulnerability weakens the WebContent↔NetworkProcess trust boundary and the per-site storage isolation model. The security model assumes a WebProcess connection can only reach storage belonging to sites it is permitted to host — the invariant isSiteAllowedForConnection exists to enforce; before the fix, every CacheStorage operation keyed only by DOMCacheIdentifier sidestepped it. An attacker able to emit arbitrary IPC to the network process could read another site's cached HTTP responses — which routinely include credentialed API responses and personal data — and could also write attacker-chosen Response objects into another site's cache, a persistent poisoning primitive that a subsequent legitimate visit would serve back as same-origin content. In effect this converts a single-site renderer compromise into cross-site data theft plus persistent cross-site content control, without any additional memory-safety bug.
Insight
The check could not previously exist because the origin was thrown away as a micro-optimization: it was materialized only when m_level < UnifiedOriginStorageLevel::Standard, since at that time the origin's only consumer was the marker file. That is a recurring shape in access-control bugs — authorization context is stored conditionally, keyed on whichever feature happened to need it first, and the later need for authorization finds the field empty. The fix's separation of "do I know my origin" (always) from "do I write an origin file" (ShouldWriteOriginFile) is the generalizable correction. Worth noting separately: the guard checks origin->topOrigin only, while Cache Storage is keyed by the full ClientOrigin pair. That is consistent with a process-per-site model where one process legitimately hosts a whole page including cross-site subframes, but it does mean the enforcement granularity is the partition, not the frame origin.
Audit directions
-
Identifier-as-capability — an IPC handler resolves a caller-supplied
ObjectIdentifierthrough a registry whose key space spans all origins/callers, then acts on the result without re-deriving the owner. The invariant is resolution must be followed by an ownership check, because the ID is attacker-supplied data and not a capability handle. Narrow: audit the remaining handlers inNetworkStorageManager.cppthat look up throughm_idbStorageRegistry,m_fileSystemStorageHandleRegistryandm_storageAreaRegistry— the tell is a handler body that begins with a registry lookup keyed only by an identifier from the message and never mentionsisSiteAllowedForConnectionororiginStorageManager(origin, ...). Wider: the same shape appears wherever WebKit keeps a process-global handle table for cross-process objects — GPU process remote-resource and rendering-backend caches, media-player andRemoteDisplayListidentifier maps; the code-search shape isHashMap<SomeIdentifier, WeakPtr<T>>(orRef<T>) as a member of a message-receiver class. Widest: this is the classic IDOR/ambient-authority class and holds in any system that names resources with forgeable integers rather than unforgeable handles — POSIX fd tables when descriptors are passed as numbers across a boundary, REST/gRPC resource IDs without per-tenant scoping, Wayland object IDs; the invariant to carry is if the ID can be typed by the caller, the ID is not authorization. -
Authorization context retained conditionally on an unrelated feature flag. The invariant is a resource-owning object must know its owner unconditionally; whether it also persists that fact to disk is a separate decision. Narrow: grep
Source/WebKit/NetworkProcess/storage/for constructors and factories takingstd::optional<WebCore::ClientOrigin>or an origin argument gated onUnifiedOriginStorageLevel—IDBStorageManager,FileSystemStorageManager,ServiceWorkerStorageManager,LocalStorageManagerandSessionStorageManagerare the peers of the manager fixed here; the tell is an origin/identity parameter that isstd::optionaland whose only read is a file-writing or logging call. Wider: any class whose identity or principal field is populated only under some build/config/level condition — search for members declaredstd::optional<Origin>,std::optional<SecurityOriginData>,std::optional<RegistrableDomain>in long-lived manager or session objects; the shape to notice is a member set in one constructor branch and never elsewhere. Widest: authorization inputs must not be conditionally-populated caches of something computed for another purpose; it applies to multi-tenant services that stash tenant IDs only when audit logging is on, and to any framework where a request-scoped principal is optional. Match tell: the check you want to add cannot be written because the field isstd::nullopton some code paths. -
Validating one component of a composite key. The invariant is the check's granularity must match the key's granularity, or the difference must be a deliberate, documented policy decision. Narrow: review every
isSiteAllowedForConnectioncall site inNetworkStorageManager.cppand confirm which derive theRegistrableDomainfromtopOriginversusclientOrigin— the patched CacheStorage handlers useRegistrableDomain { origin->topOrigin }, so establish whether any storage type in this file is intended to be enforced at frame-origin rather than partition granularity, and whether the two sets of call sites agree. Wider: the same shape appears anywhere aClientOriginpair, a partition key, or a (site, frame) tuple is reduced to one component before a comparison — search forRegistrableDomain {constructions taking a.topOriginor.clientOriginfield and check the surrounding policy intent. Widest: reducing a composite principal to a prefix before an authorization comparison silently widens the grant; it recurs in cookie and storage partitioning across all browser engines, and in multi-tenant row-level security that filters on tenant but not on sub-scope. Match tell: an access check whose argument list is strictly shorter than the key used to store the object it guards. -
Investigate whether
MESSAGE_CHECKon a value that can legitimately be absent introduces a connection-termination path reachable without malice.CacheStorageCache::origin()returnsstd::nulloptwhenever theWeakPtr<CacheStorageManager> m_managerhas been cleared, and the new checks treat that as an IPC violation. Trace the lifetimes:CacheStorageManagerholdsVector<Ref<CacheStorageCache>>andm_removedCaches, whileCacheStorageRegistryholds onlyWeakPtr<CacheStorageCache>— determine whether a cache can outlive its manager (for instance via aprotect()ed reference held across an in-flight async store callback) while still being resolvable through the registry. Wider: the class is IPC validators asserting on state that is a function of object lifetime rather than of message content; the code-search shape is anyMESSAGE_CHECKwhose predicate dereferences or tests aWeakPtr/std::optionalpopulated from another object's liveness. Verification here is not purely static — establishing whether the race is reachable would need targeted lifetime instrumentation or a stress test that closes storage while records are in flight. -
Grep the
NetworkProcess/storagemessage definition files for CacheStorage-family messages that still do not deliver anIPC::Connection&to their handler. The three handlers widened in this patch were unfixable in place precisely because the connection was never plumbed to them;cacheStorageRepresentationremains connectionless inNetworkStorageManager.h. The tell is a declaration in the// Message handlers forblock whose first parameter is notIPC::Connection&— each such handler is either provably caller-agnostic or an unchecked entry point, and the distinction should be recorded rather than assumed.