← All reports

[1] NetworkProcess parked loader claimable by any WebContent process

MediumWebKit NetworkProcess resource loadingAuthBypass

A compromised renderer could take another navigation's response just by naming it.

215a936

Medium. The damage is a confidentiality break between renderers, not memory corruption, and cashing it in needs a compromised WebContent process on the same network session. The ceiling is one navigation's main-resource response inside the parking window.

A privileged broker that hands a half-finished network load from one renderer to its successor has to make sure no other renderer can intercept it. On a process swap for a navigation response, WebKit's NetworkProcess parks the in-flight NetworkResourceLoader (the per-load object holding request parameters, network task and response delivery) in a session-wide map until the UIProcess picks the destination. The invariant is that only the chosen process resumes it, by sending ScheduleResourceLoad (the IPC message that starts or resumes a load) with the parked loader's identifier.

The angle: a compromised renderer sharing a network session with another navigation can claim that navigation's in-flight main-resource load by naming its sequential identifier, and receive a response the UI process routed to a different, possibly cross-site, process.

Add ownership check on existingLoaderToResume in NetworkConnectionToWebProcess::scheduleResourceLoad. When a BrowsingContextGroup switch is needed, the NetworkProcess parks the in-flight NetworkResourceLoader in NetworkSession::m_loadersAwaitingWebProcessTransfer, keyed by an integer ObjectIdentifier, while a new WebContent process is spun up. scheduleResourceLoad accepted the WebContent-supplied ID with no check that the caller owned the parked loader. The patch makes the UIProcess's TriggerBrowsingContextGroupSwitchForNavigation completion handler return the destination WebProcessIdentifier, records it on the cached loader, and compares it against the calling connection. Claims that arrive before the destination is known are queued (cap of 4) and resolved when it arrives; a mismatched claimant is terminated. A follow-up is planned to make the map key harder to guess. A layout test drives the check through the real ScheduleResourceLoad path using two test-only IPC messages gated behind ENABLE(IPC_TESTING_API).

Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp

if (existingLoaderToResume) {
if (CheckedPtr session = networkSession()) {
- if (auto existingLoader = session->takeLoaderAwaitingWebProcessTransfer(*existingLoaderToResume)) {
+ auto claim = session->takeLoaderAwaitingWebProcessTransfer(*existingLoaderToResume, m_webProcessIdentifier);
+ switch (claim.outcome) {
+ case NetworkSession::LoaderAwaitingWebProcessTransferOutcome::Success:
CONNECTION_RELEASE_LOG(Loading, "scheduleResourceLoad: Resuming existing NetworkResourceLoader");
- m_networkResourceLoaders.add(*identifier, *existingLoader);
- existingLoader->transferToNewWebProcess(*this, loadParameters);
+ completeQueuedExistingLoaderResume(claim.loader.releaseNonNull(), WTF::move(loadParameters));
return;
+ case NetworkSession::LoaderAwaitingWebProcessTransferOutcome::WrongCaller:
+ MESSAGE_CHECK(false);
+ return;
+ case NetworkSession::LoaderAwaitingWebProcessTransferOutcome::Pending:
+ if (!session->queuePendingLoaderClaim(*existingLoaderToResume, *this, WTF::move(loadParameters))) {
+ ...
+ MESSAGE_CHECK(false);
+ }
+ return;
...

Source/WebKit/NetworkProcess/NetworkSession.cpp

-RefPtr<NetworkResourceLoader> NetworkSession::takeLoaderAwaitingWebProcessTransfer(NetworkResourceLoadIdentifier identifier)
+NetworkSession::LoaderAwaitingWebProcessTransferClaim NetworkSession::takeLoaderAwaitingWebProcessTransfer(NetworkResourceLoadIdentifier identifier, WebCore::ProcessIdentifier callerWebProcess)
{
- auto cachedResourceLoader = m_loadersAwaitingWebProcessTransfer.take(identifier);
- return cachedResourceLoader ? cachedResourceLoader->takeLoader() : nullptr;
+ auto it = m_loadersAwaitingWebProcessTransfer.find(identifier);
+ if (it == m_loadersAwaitingWebProcessTransfer.end())
+ return { nullptr, LoaderAwaitingWebProcessTransferOutcome::NotFound };
+ auto destination = it->value->destinationWebProcess();
+ if (!destination)
+ return { nullptr, LoaderAwaitingWebProcessTransferOutcome::Pending };
+ if (*destination != callerWebProcess)
+ return { nullptr, LoaderAwaitingWebProcessTransferOutcome::WrongCaller };
+ auto cachedResourceLoader = m_loadersAwaitingWebProcessTransfer.take(identifier);
+ return { cachedResourceLoader->takeLoader(), LoaderAwaitingWebProcessTransferOutcome::Success };
}
...
+ if (connection->webProcessIdentifier() != destinationWebProcess)
+ connection->terminateForInvalidLoaderResumeClaim();
+ else if (!transferred) {
+ m_loadersAwaitingWebProcessTransfer.remove(identifier);
+ if (RefPtr loader = parkedLoader->takeLoader())
+ connection->completeQueuedExistingLoaderResume(loader.releaseNonNull(), WTF::move(claim->loadParameters));
+ transferred = true;
+ }

The change carries the UIProcess-chosen destination back to the NetworkProcess, uses it to gate the claim in takeLoaderAwaitingWebProcessTransfer, and queues early claims until the destination is known.

Destination plumbing. The completion handler of ConsiderProcessSwapForNavigationResponse used to return bool success; it now returns std::optional<WebCore::ProcessIdentifier> destinationWebProcess. The change runs through NetworkProcessProxy::considerProcessSwapForNavigationResponse, WebPageProxy::triggerBrowsingContextGroupSwitchForNavigation, triggerProcessSwapForEnhancedSecurity and performProcessSwapForNavigationResponse. The last one captures processForNavigation->coreProcessIdentifier() before continueNavigationInNewProcess. In NetworkResourceLoader::sendDidReceiveResponseWithPotentialProcessSwap, the swap-result lambda calls NetworkSession::setParkedLoaderDestinationAndResolvePendingClaims when a destination comes back, which records it on the CachedNetworkResourceLoader wrapper. The Enhanced-Security decline path uses a new, explicitly unchecked takeParkedLoaderForOriginalProcess, which returns the loader to the connection that parked it.

Checked claim. takeLoaderAwaitingWebProcessTransfer takes the caller's ProcessIdentifier and returns a LoaderAwaitingWebProcessTransferClaim with one of four outcomes: Success, NotFound, Pending or WrongCaller. scheduleResourceLoad switches on it. Success resumes through the new completeQueuedExistingLoaderResume, WrongCaller hits MESSAGE_CHECK(false), Pending calls queuePendingLoaderClaim (a full queue hits MESSAGE_CHECK), and NotFound falls through to a fresh load.

Pending claims. CachedNetworkResourceLoader gains m_destinationWebProcess, m_pendingClaims (each a PendingClaim holding a WeakPtr<NetworkConnectionToWebProcess> plus NetworkResourceLoadParameters) and maxPendingClaims = 4. When the destination arrives, the queue drains. The first matching claim gets the loader, every claim from another process calls terminateForInvalidLoaderResumeClaim (which sends NetworkProcessProxy::TerminateWebProcess), and later duplicate claims from the correct process are ignored.

Test collateral. AddSyntheticParkedLoaderForTesting and RemoveSyntheticParkedLoaderForTesting messages, a createForTesting constructor, an IPCTestingAPI getter webPageProxyIdentifierForMessageArguments, and a layout test that expects MESSAGE_CHECK on a WrongCaller claim.

Possession of a guessable object identifier is treated as proof of ownership by a privileged broker.

Where this lives. WebKit runs the WebContent, GPU, Networking and UI processes separately. WebContent is the untrusted one: NetworkProcess IPC handlers are expected to validate every sender-supplied argument as if the renderer were already compromised.

Process swap on navigation response. When a navigation response needs a different WebContent process, either for a BrowsingContextGroup change or because of the Enhanced-Security heuristics for insecure HTTP responses, the NetworkProcess does not abort the load. It parks the loader and asks the UIProcess (ConsiderProcessSwapForNavigationResponse) to choose a process. The UIProcess then calls continueNavigationInNewProcess with existingNetworkResourceLoadIdentifierToResume, and the new WebContent process sends ScheduleResourceLoad with existingLoaderToResume to adopt the parked loader instead of re-fetching.

Parked loader map. NetworkSession::m_loadersAwaitingWebProcessTransfer maps a NetworkResourceLoadIdentifier to a CachedNetworkResourceLoader, a wrapper holding a RefPtr<NetworkResourceLoader> plus an expiration timer.

Identity and validation. WebCore::ProcessIdentifier is the UIProcess-assigned identity of a WebContent process. NetworkConnectionToWebProcess stores it as m_webProcessIdentifier at creation. MESSAGE_CHECK is WebKit's IPC validation macro: when its assertion fails, the message is marked invalid and the sending process is terminated.

The root cause is a missing authorization check on an IPC handler, an insecure direct object reference. Before the fix, m_loadersAwaitingWebProcessTransfer recorded no owner for the loaders parked in it. scheduleResourceLoad took existingLoaderToResume straight from the WebContent-supplied arguments and called takeLoaderAwaitingWebProcessTransfer(*existingLoaderToResume). It then added the result to its own m_networkResourceLoaders and ran transferToNewWebProcess(*this, loadParameters), which rebinds the in-flight loader to the calling connection. The UIProcess knew the intended destination but returned only a bool to the NetworkProcess, so the NetworkProcess had no ground truth to compare the claimant against.

  UIProcess              NetworkProcess                     WebContent A        WebContent B
  ─────────              ──────────────                     ────────────        ────────────
  choose dest = B  ◄──── park loader L (key n)
  reply(bool) ─────────► map[n] = L        (no owner recorded)
                                                      ScheduleResourceLoad(resume=n)
                         take(n) ──► L bound to A  ◄─────────────┘   (pre-fix: no check)
                                                                       ScheduleResourceLoad(resume=n)
                         take(n) ──► NotFound  ◄──────────────────────────────────────┘
  After: take(n, caller) compares caller to recorded dest ─► WrongCaller ─► MESSAGE_CHECK

scheduleResourceLoad does run earlier gates, namely the first-party-for-cookies MESSAGE_CHECK and the webPageProxyID allow-list drop. They validate the claimant's own request parameters, not its relationship to the parked loader. The regression test satisfies them with its own page's URL and webPageProxyIdentifierForMessageArguments, which suggests a compromised renderer could do the same. The identifier is a NetworkResourceLoadIdentifier; the NetworkResourceLoader constructor obtains one from NetworkResourceLoadIdentifier::generate(), a WTF ObjectIdentifier generated sequentially and therefore guessable. The map key used by addLoaderAwaitingWebProcessTransfer is likely that generated ID, though the call site that derives identifier is outside the supplied context. If so, any WebContent process connected to the same NetworkSession that named the right identifier inside the parking window (up to cachedNetworkResourceLoaderLifetime) would receive the loader, and would become the target of its response delivery in place of the process the UIProcess nominated.

The fix makes the identifier a lookup key rather than a capability. The recorded destination is compared against m_webProcessIdentifier, which is passed into NetworkConnectionToWebProcess::create() and stored at construction. It is fixed for the life of the connection and cannot be chosen by the WebContent process in later messages, so it is a trustworthy claimant identity. Queuing early claims instead of granting them closes the window between parking and the UIProcess reply that a bare find-and-compare would leave open.

On exploitability, the precondition is code execution in a WebContent process that shares the victim's NetworkSession and can send arbitrary ScheduleResourceLoad messages. The bug is not reachable from script alone. The primitive is cross-process data confidentiality and integrity, not memory corruption: the claimant takes ownership of another navigation's live loader. The ceiling is loaders parked in the same session, during the parking window, for main-frame navigation responses undergoing a swap. If transferToNewWebProcess rebinds response delivery to the new connection, as its name and the m_networkResourceLoaders registration suggest, the claimant would receive that response's headers and body and the legitimate destination would lose its load. Its body is outside the supplied context, so the exact data delivered is inferred. What credential-bearing state beyond a normal response delivery would reach the claimant is the open audit point. The commit message says a follow-up will make the map key harder to guess, which is the right defense-in-depth layer but not a substitute for the ownership check.

The patch also adds a reachable state. Any WebContent process connected to the session can append to m_pendingClaims while a parked loader's destination is unknown. The cap of 4 is shared across all processes, and a full queue calls MESSAGE_CHECK(false) on whichever connection hits it. If a compromised process filled the slots before the legitimate destination's claim arrived, the legitimate process would be the one terminated, which is a narrow cross-process denial of service. The attacker's own queued claims would be terminated too when setParkedLoaderDestinationAndResolvePendingClaims runs. The two testing messages are gated behind ENABLE(IPC_TESTING_API) and should not exist in production builds.

This vulnerability weakens cross-process isolation between WebContent processes as brokered by the NetworkProcess. Process swap on navigation response assumes only the process the UIProcess selected receives the parked response, and before the fix the NetworkProcess did not enforce that. A compromised process could take over another navigation's in-flight main-resource loader and receive a response routed to a different, possibly cross-site, process, defeating the confidentiality BrowsingContextGroup and Enhanced-Security separation are meant to provide.

Takeaway: an identifier passed over IPC is a lookup key, never an authorization token. Bind the resource to the principal the authority chose, and check the connection-bound identity on every claim.