[4] LoadImageForDecoding accepted arbitrary schemes and spoofed first parties
One field of the request was checked. The ones carrying authority were not.
High. A network-process handler that fetches bytes on the renderer's behalf validated one property of one field on a struct carrying URL, first party, and cookie attribution. Not a first-stage bug — it needs a renderer compromise — but from there it is a direct read across the sandbox boundary with no further conditions.
WebKit splits work across processes: the WebContent process renders untrusted content in a tight sandbox, while the NetworkProcess performs network and cache I/O under a different sandbox profile. WebContent asks the NetworkProcess to do work by sending IPC messages, and NetworkConnectionToWebProcess is the per-renderer message receiver on the network side that services resource-loading requests. Those requests arrive as a WebCore::ResourceRequest — a compound serialized struct carrying not only the URL but firstPartyForCookies, isTopSite, sameSiteDisposition, allowCookies, and arbitrary header fields — and the receiver is expected to treat every field as attacker-controlled.
The angle: a compromised renderer can point this handler at file:///private/etc/hosts, or at a cross-origin HTTPS URL under a spoofed first party, and get the fetched bytes handed back into its own address space.
The commit message is unambiguous about both legs:
LoadImageForDecodingaccepted arbitraryResourceRequestfields with only aurl.isValid()check. This allowedfile://reads of NetworkProcess-sandbox files and credentialed cross-origin body reads via spoofedfirstPartyForCookies. Restrict the URL to HTTP(S) and enforceallowsFirstPartyForCookies, matching every other cookie-touching IPC entry point.
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
Source/WebKit/UIProcess/WebPageProxy.cpp
LayoutTests/ipc/load-image-for-decoding-file-url.html
Patch Details
Two entry points that fetch an image on behalf of a client are tightened. NetworkConnectionToWebProcess::loadImageForDecoding() previously guarded the IPC-supplied ResourceRequest with a single MESSAGE_CHECK_COMPLETION(url.isValid(), ...). The patch extends that check to url.isValid() && url.protocolIsInHTTPFamily(), and adds a second MESSAGE_CHECK_COMPLETION requiring m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow. Both failures invoke the completion handler with an empty ResourceError and then trip the message-check path for the connection.
WebPageProxy::loadAndDecodeImage() gains the same scheme restriction in its early-out: isClosed() || !request.url().isValid() || !request.url().protocolIsInHTTPFamily(). The collateral hunks are a new IPCTestingAPI layout test that hand-serializes a LoadImageForDecoding message carrying m_url and m_firstPartyForCookies set to file:///private/etc/hosts, sends it directly on the Networking connection, and asserts that the resulting invalid-message string mentions protocolIsInHTTPFamily.
Partial validation of a compound, attacker-serialized request struct — one field's well-formedness checked, while the fields carrying authority are trusted as sent.
Background
Where this lives. WebKit splits work across processes — the WebContent process renders untrusted web content in a tight sandbox, the NetworkProcess performs network and cache I/O under a different sandbox profile. The NetworkProcess treats every field of an incoming message as attacker-controlled, since a renderer compromise is an assumed threat.
MESSAGE_CHECK_COMPLETION.
A WebKit macro used in IPC handlers; when the asserted condition is false it runs the supplied completion expression (so the async reply is not dropped) and reports an invalid message on the connection, which terminates the misbehaving sender. The IPCTestingAPI exposes the resulting invalid-message string to test code via TakeInvalidMessageStringForTesting.
WebCore::ResourceRequest.
A compound serializable type carrying the URL plus a large set of loading attributes — firstPartyForCookies, httpMethod, header fields, cache policy, sameSiteDisposition, isTopSite, allowCookies, priority, requester. All of these cross the IPC boundary as part of the RequestData variant.
URL::isValid() vs URL::protocolIsInHTTPFamily().
isValid() reports whether the string parsed into a structurally well-formed URL; protocolIsInHTTPFamily() reports whether the scheme is http or https. They are independent properties.
firstPartyForCookies and its guard.
firstPartyForCookies is the URL the cookie machinery treats as the top-level site for a given load; it drives SameSite enforcement and cookie partitioning decisions. NetworkProcess::allowsFirstPartyForCookies(processIdentifier, url) is the NetworkProcess-side registry check answering whether a particular web process has been granted the right to claim a given first party, returning an AllowCookieAccess enum whose Allow value is the only passing state. It is the standard guard on cookie-touching IPC entry points.
Reply shape.
The handler completes with Expected<Ref<WebCore::FragmentedSharedBuffer>, WebCore::ResourceError> — the fetched bytes themselves travel back to the caller.
IPCTestingAPI.
A test-only facility, gated behind the IPCTestingAPIEnabled test-runner flag, that lets a layout test hand-serialize and send raw IPC messages on a named process connection — used here to simulate a compromised renderer.
Analysis
This is missing input validation at a privilege boundary, producing two distinct consequences: a confused-deputy local file read (SSRF-shaped information disclosure across the sandbox) and a cookie-attribution spoof (first-party policy bypass leading to credentialed cross-origin body disclosure).
Pre-fix Post-fix
WebContent (compromised) WebContent (compromised)
│ LoadImageForDecoding │ LoadImageForDecoding
▼ ▼
┌─ NetworkProcess ───────────┐ ┌─ NetworkProcess ───────────┐
│ url.isValid() ✓ │ │ isValid() && HTTPFamily ✗ │
│ scheme: (unchecked)│ │ allowsFirstParty… ✗ │
│ firstParty: (unchecked)│ │ └─► invalid message, │
│ └─► fetch file:// ────┼──┐ │ connection killed │
└────────────────────────────┘ │ └────────────────────────────┘
FragmentedSharedBuffer ◄───┘
(bytes returned to sender)
The handler received a fully attacker-serializable ResourceRequest and validated exactly one property of one field: url.isValid(), which only asserts the URL parsed into a well-formed URL and says nothing about the scheme. The two missing invariants are the ones the rest of the NetworkProcess IPC surface enforces on request-bearing messages: the request URL must be in the HTTP family, so that a lower-privileged process cannot direct the NetworkProcess to read from a scheme resolving against the NetworkProcess's own resource namespace; and firstPartyForCookies must be a value the sending web process is registered as allowed to claim.
The handler passes the request through to the network session's loader and returns the fetched bytes to the caller as Expected<Ref<FragmentedSharedBuffer>, ResourceError> — the raw response body is handed back over IPC to the requesting process, not merely decoded pixels rendered somewhere the requester already controls. That reply shape is what turns both missing checks into disclosure primitives. On the scheme axis: because the URL scheme was unconstrained, a file:// URL passed isValid(), and the fetch was performed by the NetworkProcess, whose sandbox profile differs from WebContent's. On the cookie axis: with no allowsFirstPartyForCookies check, the sender could set firstPartyForCookies — and, in the same serialized struct, m_isTopSite and m_sameSiteDisposition — to the victim origin while requesting a cross-origin URL, so the load would be issued with the victim's cookies attached and the response body returned to the sender. The test case demonstrates the shape precisely, constructing the RequestData variant by hand with m_url and m_firstPartyForCookies both file:///private/etc/hosts, m_allowCookies: true, and m_isTopSite: true.
The precondition is an already-compromised (or IPC-capable) WebContent process; loadImageForDecoding is a NetworkProcess IPC handler, not a web-exposed API, so ordinary JavaScript cannot reach it without either a prior renderer compromise or the test-only IPCTestingAPI. Given that precondition, the test in this commit is the trigger recipe: construct a RequestData variant with m_url set to the target, set m_firstPartyForCookies, m_allowCookies: true and m_isTopSite: true to taste, serialize with the NetworkConnectionToWebProcess_LoadImageForDecoding message definition along with the process's own pageID and an attacker-chosen maximumBytesFromNetwork, and send it on the Networking connection.
Two directions follow. On the scheme leg, with m_url set to a file:// path, the pre-fix isValid()-only guard passes and the load would be serviced by the NetworkProcess's session rather than the renderer's; because the completion handler returns a FragmentedSharedBuffer of the fetched bytes to the sender, the file contents would be readable by the compromised renderer, bounded by maximumBytesFromNetwork which the sender also controls. The commit message states this reached NetworkProcess-sandbox files; the supplied context stops at the MESSAGE_CHECK and does not include the NetworkLoad/session code path that would show file:// actually being serviced, so that leg is relayed as the author's claim. On the cookie leg, requesting a cross-origin HTTPS URL while setting m_firstPartyForCookies to the victim's site means the load would be attributed as first-party for SameSite and partitioning purposes, so credentialed responses could be fetched and their bodies returned over the same completion handler. Realising that additionally requires that the target endpoint serves content the cookie policy would otherwise have withheld and that no separate CORS/response-filtering step sits between the loader and the completion handler — the supplied context does not include that portion of the load path.
This vulnerability weakens the WebContent-to-NetworkProcess privilege boundary and, separately, the cookie/same-site policy boundary. The security model assumes two things about request-bearing IPC from a renderer: that the renderer cannot use the NetworkProcess as a deputy to reach resource namespaces outside its own sandbox, and that a renderer cannot assert an arbitrary first-party identity for cookie-attachment decisions — the latter is exactly what allowsFirstPartyForCookies() exists to enforce. Before the fix, an attacker who had already compromised a WebContent process could have used this message as a read primitive against files reachable from the NetworkProcess sandbox, and could have issued credentialed cross-origin loads under a spoofed first party while receiving the response body back over IPC. The gain is information disclosure — local file contents and cross-origin authenticated response bodies — which is chain material for privilege escalation and cross-site data theft, not memory corruption.
Insight
The commit message's own framing — "matching every other cookie-touching IPC entry point" — is the interesting part. allowsFirstPartyForCookies is an established, widely applied guard; this handler was simply the one that did not get it. That is the signature of an entry point added later than the convention it should have followed, and it suggests the productive audit unit is not "find missing bounds checks" but "enumerate every IPC handler that accepts a ResourceRequest and diff its guard prologue against the convention." Worth noting too that the fix constrains only firstPartyForCookies and the scheme; the same serialized RequestData still carries m_isTopSite, m_sameSiteDisposition, m_allowCookies, and arbitrary m_httpHeaderFields from the renderer, and the test explicitly sets m_isTopSite: true. Whether those remaining renderer-supplied trust signals are independently re-derived on the NetworkProcess side is a separate question this patch does not answer.
Audit directions
-
Partial validation of a compound cross-privilege struct. A privileged process accepts a serialized struct from a lower-privileged one and validates a single field's well-formedness while treating the authority-bearing fields as trusted. The invariant is every field of a cross-privilege struct that feeds an authorization or namespace decision must be independently re-validated, not just the field that is easiest to check. Narrow: enumerate the handlers in
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cppandNetworkProcess.cppwhose signature takesWebCore::ResourceRequest&&orNetworkResourceLoadParameters&&, and diff each one'sMESSAGE_CHECK/MESSAGE_CHECK_COMPLETIONprologue against the pair this commit installs — match tell is a handler whose only guard is a singleisValid()-style predicate on one member of a multi-field struct. Wider: the same shape appears whenever any WebKit process boundary deserializes a rich type and checks one member — apply it toClientOrigin,SecurityOriginData, andWebCore::ResourceResponsearguments on the GPUProcess and UIProcess message surfaces; match tell in code-search results is a handler body where the number of struct members far exceeds the number of predicates in the guard prologue. Widest: this is the general "partial validation of a compound message at a trust boundary" class and holds in any IPC/RPC system that hands the receiver a struct instead of scalars — Chromium's MojoStructTraits::Read, Android BinderParcelreaders, and kernelcopy_from_useron a user-supplied struct all exhibit it. The invariant to carry across codebases: for each field, ask "does the receiver make a decision from this field, and if so, who is entitled to set it?" -
URL-scheme confused deputy. A privileged fetcher accepts a URL from a lower-privileged caller and resolves it against the privileged process's own resource namespace. The invariant is a URL that crosses inward across a privilege boundary must be scheme-restricted to the schemes the caller could already reach itself. Narrow: grep the NetworkProcess IPC handlers for
url.isValid()occurrences not accompanied byprotocolIsInHTTPFamily()or an equivalent scheme allowlist, and check preconnect, ping, and speculative-load entry points first — match tell is aMESSAGE_CHECKwhose predicate constrains parseability but not scheme. Wider: the same class covers any privileged component that takes a caller-supplied locator and dereferences it — custom URL scheme handler registration, blob anddata:URL resolution across process boundaries, and file-backedSandboxExtensionconsumption; match tell is a call site where a locator is handed to a resolver without a preceding allowlist. Widest: this is classic SSRF/confused-deputy generalized past HTTP — it applies to server-side URL fetchers (curl-backed webhooks, image proxies, PDF renderers) and to any local privileged daemon that opens a client-named path. The invariant to carry across codebases: whenever a component with more reach than its caller dereferences a caller-supplied name, the scheme/namespace must be allowlisted at the boundary, never denylisted downstream. -
Client-asserted trust labels used in an authorization decision.
firstPartyForCookiesis now guarded here, but the same serializedRequestDatastill carriesm_isTopSite,m_sameSiteDisposition,m_allowCookies, andm_httpHeaderFieldsfrom the renderer, and this commit's own test setsm_isTopSite: true. Narrow: trace every consumer ofNetworkProcess::allowsFirstPartyForCookiesand, for each, check whether the sibling attribution fields on the same request are re-derived or accepted as sent — match tell is a cookie or partitioning decision that readsrequest.isTopSite()orrequest.sameSiteDisposition()on a request that arrived over IPC. Wider: examine the other renderer-supplied identity signals crossing into the NetworkProcess and the storage layer —ClientOrigin,WebPageProxyIdentifier, andSecurityOriginDataarguments; match tell is any function that takes both a process identifier and an origin-like value but consults only one of them. Widest: this is the general "caller supplies its own identity claim" class and holds wherever a request carries both a channel-bound identity and a self-asserted one — unvalidated JWT claims,X-Forwarded-Fortrusted at the app tier, tenant IDs read from the request body. The invariant to carry across codebases: an identity used for an authorization decision must be bound to the channel the request arrived on, never read from the request payload. -
Completion-path behaviour of newly added
MESSAGE_CHECK_COMPLETIONguards in async handlers. A validation macro that both fails a request and terminates a connection must still satisfy the async-reply contract, or the caller-side completion handler leaks or the reply is dropped. Narrow: grepSource/WebKit/NetworkProcessforMESSAGE_CHECK_COMPLETIONin functions whose last parameter is aCompletionHandler, and confirm the completion expression constructs a valid failure value on every early-exit path — match tell is aMESSAGE_CHECK_COMPLETIONwhose completion argument differs in shape from the handler's success-path completion call. Wider: the same shape occurs in any async handler with multiple guard-then-return points, including the GPUProcess remote-object surfaces; match tell is a function with morereturnstatements than distinct completion-handler invocations. This class is bound to WebKit'sMESSAGE_CHECK_COMPLETION_BASEidiom, which fuses validation failure with connection teardown; Chromium's Mojo separatesReportBadMessagefrom callback resolution, so the exact shape does not transfer and the audit ceiling is WebKit's IPC boundary.