[4] Cookie access validation for startDownload() and convertMainResourceLoadToDownload()
The download path took the renderer's word for which site it was on.
Medium — two download entry points accepted a renderer-chosen first-party value that every other cookie-sensitive load path validates. The gap needs a prior renderer compromise to reach and yields no memory-corruption primitive, but it converts that compromise into authenticated state-changing requests against every site with live cookies.
WebKit runs network loads in a separate NetworkProcess that owns the cookie jar, precisely so that a sandboxed renderer cannot dictate which cookies are attached to a request. Every ResourceRequest carries a firstPartyForCookies URL naming the top-level site the request belongs to, and the cookie layer uses it to decide SameSite attachment and third-party blocking. The network process is supposed to independently confirm that the sending renderer is actually hosting content for the domain it names, rather than taking the claim at face value.
The angle: a compromised renderer can start a download whose request names any victim site as its first party, and the network process attaches that site's SameSite-restricted cookies to an attacker-chosen endpoint — CSRF that the server sees as user-initiated first-party traffic.
The commit message is explicit: NetworkConnectionToWebProcess::startDownload() and convertMainResourceLoadToDownload() do not validate the firstPartyForCookies in the requests sent to them by the web process, so a web process could send in a cross-site origin, leading to requests being sent to that cross-site origin containing the cookies for it — requests that would be seen as legitimate even though they did not come from the user. The fix adds message checks confirming the web process actually has cookie access to the origin it names. The check had to be made conditional on a non-empty firstPartyForCookies because unconditional checking crashed at the ASSERT_NOT_REACHED() in NetworkProcess::allowsFirstPartyForCookies for tests such as TEST(_WKDownload, DownloadRequestOriginalURLDirectDownload), since the PolicyAction::Download path never adds the web process to the map.
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
Patch Details
Both handlers gain an input-validation guard before forwarding the WebProcess-supplied ResourceRequest to DownloadManager: MESSAGE_CHECK(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow), which via MESSAGE_CHECK_BASE(assertion, this->connection()) tears down the offending IPC connection when the assertion fails. The guard is deliberately conditional, running only if (!request.firstPartyForCookies().isEmpty()). In convertMainResourceLoadToDownload() the check sits after the existing RELEASE_ASSERT(RunLoop::isMain()) and before the !mainResourceLoadIdentifier fallback that calls DownloadManager::startDownload directly. No other files, tests, or call sites are modified.
Privileged component trusting a security-policy field supplied by the untrusted component it is meant to arbitrate for, on one entry point while validating it on all the others.
Background
Process model. WebContent processes are sandboxed and treated as fully untrusted — any renderer bug is assumed to end in arbitrary code execution there. The NetworkProcess is a separate, more privileged process that owns the cookie jar and performs all actual network I/O; every message crossing that boundary is attacker-controlled by assumption.
firstPartyForCookies. A URL carried on every WebCore::ResourceRequest naming the top-level site the request belongs to. The cookie layer uses it to decide whether a request is first-party or third-party, which drives SameSite=Lax/Strict cookie attachment and third-party-cookie blocking.
NetworkProcess::allowsFirstPartyForCookies(ProcessIdentifier, URL). The network process's authoritative check that a given web process is actually hosting content for a given first-party domain. It returns a NetworkProcess::AllowCookieAccess tri-state (Allow being the only accepting value in this comparison) based on a map of process → permitted first-party domains that is populated as pages load in that process.
MESSAGE_CHECK. WebKit's IPC-validation macro. Here it expands to MESSAGE_CHECK_BASE(assertion, this->connection()) — on failure it treats the message as malformed and tears down the connection to the sending process, the standard response to a renderer that has sent something it should be incapable of sending.
Download entry points. startDownload() begins a download from a fresh request; convertMainResourceLoadToDownload() converts an in-flight main-resource load into a download and, when no mainResourceLoadIdentifier is supplied, falls through to DownloadManager::startDownload() as well. Both end in DownloadManager::startDownload(), which builds NetworkLoadParameters from the request, sets StoredCredentialsPolicy::Use for non-ephemeral sessions, and creates a PendingDownload that performs the load.
CSRF. An attack in which a request is issued to a target site carrying the victim's ambient credentials, so the server executes it as if the user had initiated it.
Analysis
The two handlers accepted a ResourceRequest straight off the IPC wire and handed it to DownloadManager::startDownload() without validating any of its cookie-policy-bearing fields:
Before: After:
WebContent WebContent
└─► StartDownload(req) └─► StartDownload(req)
│ firstPartyForCookies = victim │
▼ ▼
NetworkProcess NetworkProcess
└─► DownloadManager::startDownload ├─ firstParty non-empty?
└─► NetworkLoadParameters │ └─► allowsFirstPartyForCookies(pid, fp)
StoredCredentialsPolicy::Use │ └─ !Allow ──► MESSAGE_CHECK: kill connection
└─► victim's cookies sent └─► DownloadManager::startDownload
The supplied DownloadManager::startDownload() source confirms the request is forwarded verbatim into NetworkLoadParameters parameters; parameters.request = request; and that credentials are enabled for non-ephemeral sessions (parameters.storedCredentialsPolicy = sessionID.isEphemeral() ? StoredCredentialsPolicy::DoNotUse : StoredCredentialsPolicy::Use;) before PendingDownload::create(...) issues the network load — so the forged first-party value reaches a real, credentialed outbound request. This is a logic error, not a memory-safety bug: nothing is corrupted, the wrong policy is applied.
The precondition is a compromised WebContent process — a normal renderer populates firstPartyForCookies from the document it is actually hosting, so this is not reachable by scripting alone. Given renderer code execution, the attacker forges a StartDownload or ConvertMainResourceLoadToDownload IPC message whose ResourceRequest points its URL at a state-changing endpoint on the victim site and sets firstPartyForCookies to that victim site's URL. Before the fix nothing compared that value against m_webProcessIdentifier, so the resulting PendingDownload load carried the victim's SameSite-restricted cookies to an endpoint the attacker chose. Repeating this per target site gives a broad authenticated-request primitive across every site with live cookies in the profile.
This change also leaves one intentional gap. The guard is conditional on !request.firstPartyForCookies().isEmpty(), so an empty first-party value still reaches DownloadManager::startDownload() with no process-to-origin check — the reachable state that remains is "download initiated by a web process for which no first-party relationship has been asserted". The assumption held rather than enforced is that an empty firstPartyForCookies cannot impersonate a specific victim site and that the resulting cookie classification is at least as restrictive as third-party. If any downstream code path treats an empty first-party as same-site by default, or derives it later from request.url(), the CSRF condition could reappear through the empty-value branch. The carve-out exists because the PolicyAction::Download path never registers the process in the map allowsFirstPartyForCookies consults, which is itself an enforcement gap worth closing at its source rather than at the check site.
This vulnerability weakened the WebContent-to-NetworkProcess trust boundary. WebKit's security model assumes the network process independently verifies that a renderer is entitled to the origins it names, so cookie attachment and SameSite classification cannot be dictated by the sandboxed side; before the fix these two download entry points violated that. An attacker with renderer code execution could have caused the network process to issue credentialed cross-site requests bearing a victim site's cookies, with the victim's server seeing them as legitimate first-party, user-initiated traffic — converting renderer compromise into authenticated state-changing actions on unrelated sites without defeating any additional sandbox layer.
The interesting part of this commit is not the missing check but why it could not simply be added unconditionally. The honest fix — validate always — fired an ASSERT_NOT_REACHED() on legitimate downloads, and the author worked around it by exempting empty first-party values. That is a recurring shape in IPC hardening: the validator's backing state is populated by one subsystem (page loads) while the entry point being hardened belongs to another (downloads), and the two have divergent registration lifecycles. Wherever a validator's ground-truth map is populated as a side effect of a different operation than the one being validated, expect either false rejections or, as here, a carve-out that reintroduces a narrower version of the original gap. The download/navigation-policy path is a good general hunting ground: it is the branch where a load stops being a load, so per-load bookkeeping tends to be skipped.
Audit directions
-
Policy field validated on most entry points but not all. The invariant is every IPC handler that consumes an attacker-controllable policy field must apply the same validator, with no entry point exempt. Narrow: grep
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cppandNetworkResourceLoader.cppfor handlers taking aResourceRequest,NetworkResourceLoadParameters,ClientOrigin, orSecurityOriginDataparameter and diff that set against the call sites ofallowsFirstPartyForCookies— code-review tell: a handler that readsrequest.firstPartyForCookies(),topOrigin, orclientOriginand forwards it without an interveningMESSAGE_CHECK. Wider: the same class appears wherever a privileged process re-derives policy from a field the sandboxed process chose — auditWebSWServerConnection,WebSharedWorkerServerConnection,NetworkStorageManager, andNetworkSocketChannelhandlers for origin/first-party parameters, since those are separate message receivers with their own validation discipline; the tell in search results is a message handler whose first statement is not a check. Widest: the trusted side must never accept the untrusted side's assertion about its own identity or entitlement — applies to Chromium's Mojo receivers withRenderFrameHost-bound origin arguments, any server API taking a caller-supplied tenant/account ID, and token-issuing services that accept a claimedsub. Carry the question: which field in this message names a principal, and does the receiver re-derive it from a channel it controls or read it from the message? -
A validator whose backing state is populated by a different subsystem than the one being validated. The invariant is a validator's ground-truth registry must be populated on every path that can reach the validated operation. Narrow: trace who calls the registration side that populates the map behind
NetworkProcess::allowsFirstPartyForCookiesand confirm whether thePolicyAction::Downloaddecision path is the only omission — match tell: a policy-decision branch that returns early before the per-load bookkeeping other actions perform. Wider: the same shape recurs in any check written asif (fieldIsPresent) validate(field)— grep WebKit's NetworkProcess and GPUProcess IPC handlers forMESSAGE_CHECKguarded by an emptiness,std::optionalengagement, orisNull()test, and ask for each whether the skipped branch is genuinely benign or merely untested; code-review tell: aMESSAGE_CHECKthat is not the first statement of the handler. Widest: conditional validation is a bypass unless the exempted input class is provably harmless — applies to allowlist middleware that skips checks on absent headers, schema validators that treat missing fields as optional, authorization layers that no-op on null subjects. -
Operations that change a request's kind mid-flight and escape the checks attached to its original kind. The invariant is a state transition must not drop the validations that applied to either the source or the destination state. Narrow: audit the other conversion-shaped handlers around
DownloadManager—convertNetworkLoadToDownload,dataTaskBecameDownloadTask,resumeDownload,publishDownloadProgress— for parameters that originate in the web process and were validated only on the pre-conversion path; code-review tell: a handler that reconstructs aResourceRequestor destination path from IPC input rather than carrying forward the already-validated object. Wider: the same class covers redirect handling, service-worker fetch interception, and blob-URL resolution, all of which swap the effective request identity partway through — the shape to notice is any function whose name containsconvert,becameX,resume, ortakeOverand whose arguments include an origin, URL, or credential policy. Widest: validation attached to an object's initial classification must be re-asserted whenever the classification changes — HTTP request upgrades to WebSocket, file handles re-opened with new modes, OAuth token exchanges.