← All reports

[6] LoadImageForDecoding MESSAGE_CHECK tripped on legitimate embedder calls

LowWebKit NetworkingAuthBypass

cf2d405

Low — the failure mode is a spurious process kill, not a policy bypass; the cookie validator was unconditional for every value that reached it. What keeps it above "not a bug" is that ordinary embedder API usage triggered it, and the repair relocates where cookie authority is derived.

WebKit's MESSAGE_CHECK family encodes a specific claim: the checked field is an invariant the sending process is responsible for maintaining, so a violation means the sender is compromised and should be killed. That claim only holds when the sender actually authors the value. The _loadAndDecodeImage: SPI carries an app-supplied NSURLRequest from the UI process, through the WebContent process, into the Networking process, with the application's own mainDocumentURL still attached — and that field arrives at a fail-stop validator in a process that never saw who set it.

The angle: for defensive value — an app calling _loadAndDecodeImage: before any navigation, or with its own mainDocumentURL, had its WebContent process terminated; the audit lesson is which fields belong behind a MESSAGE_CHECK at all.

The commit message enumerates the two surviving failure modes from an earlier incomplete fix:

There are two cases where the MESSAGE_CHECK in NetworkConnectionToWebProcess::loadImageForDecoding can still fail after the fix in rdar://176310702: a client set an incorrect or empty mainDocumentURL; a client called _loadAndDecodeImage: on a WKWebView that has not loaded any content. Set firstPartyForCookies in the web process from page->mainFrameURL(), which is the authoritative value, and drop the WKWebView-side fallback added in the previous fix. When mainFrameURL is empty (no navigation has happened yet, so there is no cookie context), disable cookies for the request instead of failing the MESSAGE_CHECK.

Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp

void NetworkConnectionToWebProcess::loadImageForDecoding(WebCore::ResourceRequest request, ...)
{
auto url = request.url();
MESSAGE_CHECK_COMPLETION(url.isValid() && url.protocolIsInHTTPFamily(), completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
- MESSAGE_CHECK_COMPLETION(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow, completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
+
+ if (request.firstPartyForCookies().isValid())
+ MESSAGE_CHECK_COMPLETION(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow, completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
+ else
+ request.setAllowCookies(false);

Source/WebKit/WebProcess/WebPage/WebPage.cpp

void WebPage::loadAndDecodeImage(WebCore::ResourceRequest&& request, ...)
{
- URL url = request.url();
+ auto url = request.url();
+ RefPtr page = corePage();
+ if (!page)
+ return completionHandler(makeUnexpected(decodeError(url)));
+
+ request.setFirstPartyForCookies(page->mainFrameURL());
WebProcess::singleton().ensureNetworkProcessConnection().connection().sendWithAsyncReply(Messages::NetworkConnectionToWebProcess::LoadImageForDecoding(WTF::move(request), ...

Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm

- (void)_loadAndDecodeImage:(NSURLRequest *)request constrainedToSize:(CGSize)maxSize ...
{
- RetainPtr<NSMutableURLRequest> mutableRequest;
- if (!request.mainDocumentURL) {
- mutableRequest = adoptNS([request mutableCopy]);
- [mutableRequest setMainDocumentURL:self.URL];
- request = mutableRequest.get();
- }
-
WebCore::ResourceRequest resourceRequest(request);

Tools/TestWebKitAPI/Tests/WebKit/WKWebView/LoadAndDecodeImage.mm

+TEST(WebKit, LoadAndDecodeImageBeforeAnyNavigation)
+{
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] init]);
+ ...
+ EXPECT_NULL([webView URL]);
+ [webView _loadAndDecodeImage:server.request("/image.png"_s) ... ];
+ EXPECT_NOT_NULL(resultImage);
+ EXPECT_FALSE(processTerminated);
+}
+
+TEST(WebKit, LoadAndDecodeImageWithCallerProvidedDisallowedMainDocumentURL)
+{
+ [webView synchronouslyLoadRequest:server.request()];
+ RetainPtr request = adoptNS([[NSMutableURLRequest alloc] initWithURL:server.request("/image.png"_s).URL]);
+ [request setMainDocumentURL:[NSURL URLWithString:@"http://unrelated-domain.example/"]];
+ ...
+ EXPECT_FALSE(processTerminated);
+}

The change spans three layers — it moves cookie-context derivation into the web process, relaxes the network-process validator for the unset case, and deletes the now-redundant UI-process fallback.

In WebPage.cpp, WebPage::loadAndDecodeImage now resolves corePage() first (bailing out via decodeError(url) if there is no page) and unconditionally overwrites the incoming request's cookie context with request.setFirstPartyForCookies(page->mainFrameURL()) before forwarding the IPC — so the value reaching the network process is derived in-process from the page's own main-frame URL rather than from whatever mainDocumentURL the API client attached.

In NetworkConnectionToWebProcess.cpp, the previously unconditional allowsFirstPartyForCookies MESSAGE_CHECK_COMPLETION is now gated on request.firstPartyForCookies().isValid(); when the first-party URL is not valid the validator is skipped and request.setAllowCookies(false) is applied instead of terminating the sender. The URL-validity/HTTP-family check is untouched.

In WKWebView.mm, -_loadAndDecodeImage:... drops the UI-process fallback added by the previous fix (the NSMutableURLRequest copy that set mainDocumentURL to self.URL). Both new tests assert the image decodes and, critically, EXPECT_FALSE(processTerminated).

Applying a fail-stop compromise-detection validator to a field that originates outside the validated trust boundary and has a legitimate unset state.

Process split. WKWebKit runs the embedding app and UI in one process, page content in a sandboxed WebContent process, and all networking in a separate Networking process. The Networking process treats WebContent as untrusted and validates its messages.

firstPartyForCookies. A field on WebCore::ResourceRequest holding the URL of the top-level document a subresource load belongs to; the network stack uses it as the cookie/partitioning context for the request. On Cocoa it maps to NSURLRequest.mainDocumentURL.

MESSAGE_CHECK_COMPLETION. WebKit's IPC validation macro. When the asserted condition is false it runs the supplied completion handler and then treats the incoming message as invalid, which terminates the connection to the sending process. In NetworkConnectionToWebProcess.cpp it is bound to this->connection(). Its design intent is compromise detection, not input sanitation — which is what makes the choice of what to put behind it a design decision rather than a formality.

NetworkProcess::allowsFirstPartyForCookies(processIdentifier, url). The network process's registry check answering "has this web process been told it may load with this first-party cookie context?", returning a NetworkProcess::AllowCookieAccess value (Allow being the passing one). The UI process registers permitted first parties per web process as navigations happen.

Page::mainFrameURL() and URL::isValid(). mainFrameURL() is the WebCore-side URL of the page's main frame — the top-level document currently loaded in that WebPage; it is empty when no navigation has occurred. URL::isValid() is true when the URL parsed into a well-formed URL; an empty or unparseable string yields an invalid URL.

ResourceRequest::setAllowCookies(false). Marks a request so the platform network layer does not attach or store cookies for it (on Cocoa, HTTPShouldHandleCookies off).

-[WKWebView _loadAndDecodeImage:...]. A private WKWebView API for embedding applications that fetches an image URL through the web page's network context and returns a decoded platform image. It runs UI process -> WebPage::loadAndDecodeImage (WebContent) -> NetworkConnectionToWebProcess::loadImageForDecoding (Networking), so the request object crosses two process boundaries with the app's fields still attached.

This is an IPC validator misclassification — a policy error causing spurious process termination, not memory corruption.

  Pre-fix                                    Post-fix
  App: NSURLRequest.mainDocumentURL          App: mainDocumentURL (ignored)
    │  (may be nil / unrelated origin)          │
    ▼ UI process                                ▼ UI process
  WKWebView: backfill from self.URL          WKWebView: pass through
    │  (nil if never navigated)                 │
    ▼ WebContent                                ▼ WebContent
  forward request verbatim                   setFirstPartyForCookies(mainFrameURL)
    │                                           │
    ▼ Networking                                ▼ Networking
  MESSAGE_CHECK(allowsFirstParty…)           isValid() ? MESSAGE_CHECK(…)
    └─► FAIL ► kill WebContent                          : setAllowCookies(false)

WebPage::loadAndDecodeImage forwarded the ResourceRequest essentially verbatim, so firstPartyForCookies was whatever the embedding application had put in NSURLRequest.mainDocumentURL (with a UI-process fallback to self.URL from the earlier fix). On the receiving side, loadImageForDecoding treated that field as an attested value and ran it through a validator whose failure semantics are "the sender is malicious, kill it."

The violated invariant is the one every MESSAGE_CHECK implicitly asserts: the checked field is something the sending process is responsible for maintaining, so a violation is evidence of compromise. Here the field was not sender-controlled in that sense — it flowed in from outside the WebContent trust boundary, and it had a legitimate "no value yet" state. The commit message enumerates the two surviving failure modes exactly, and both became tests. In the first case, allowsFirstPartyForCookies returns something other than Allow because http://unrelated-domain.example/ was never registered for that web process identifier. In the second, there is no valid URL to check at all — the LoadAndDecodeImageBeforeAnyNavigation test's EXPECT_NULL([webView URL]) pins the state where the UI-process fallback self.URL is nil and firstPartyForCookies ends up empty. (The crash-report signature in the bug title and the attribution to rdar://176310702 come from the commit message; neither the prior commit nor the crash record is part of the supplied context.)

The fix splits the two concerns. The authoritative cookie context is now computed where the authority actually lives — page->mainFrameURL() inside the web process — so the network-process validator is once again checking a value the sender is responsible for. The "no cookie context exists yet" case is handled by fail-closed degradation rather than by the fail-stop validator, which is the correct disposition for a legitimate state rather than a compromise signal.

This change relaxes an IPC validator, so the surface question is worth stating explicitly. NetworkConnectionToWebProcess::loadImageForDecoding now has a path that reaches networkSession() and issues a real network load without having consulted allowsFirstPartyForCookies at all — previously every reaching request had passed that check. Any WebContent process, including a compromised one, can select this path simply by sending a firstPartyForCookies that fails URL::isValid(). What is assumed rather than enforced is that setAllowCookies(false) is honored end-to-end for this load, and that every other consumer of firstPartyForCookies downstream behaves safely when the field is empty: the field keys more than cookie attachment in WebKit — it feeds cache partitioning, same-site determination and tracking-prevention decisions — and the new branch only neutralizes the cookie dimension. If those assumptions break, the class of bug that would become possible is partition confusion rather than memory corruption: a load attributed to an empty first-party key could share a cache or storage partition with unrelated first parties. The commit does not show the downstream handling of setAllowCookies(false) for the image-decode path, so this is a surface to audit rather than an identified defect.

On exploitability, there is no attacker gain to extract. Reachability from web content is absent: WebPage::loadAndDecodeImage is driven by the _loadAndDecodeImage: SPI, not by any JavaScript, HTML, or CSS-reachable binding, so page script cannot originate the LoadImageForDecoding message through supported paths — layout tests reach it only through the IPC_TESTING_API harness, which is not compiled into shipping builds. The realistic pre-fix trigger is an embedder calling the SPI before any navigation, or with mainDocumentURL set to a host the UI process never registered, in both cases tripping the validator and tearing down the WebContent process. The one actor who can reach the vulnerable branch deliberately is an already-compromised WebContent process, and the only outcome available to it is its own termination, which is not an attacker transition.

The trust boundary at stake is WebContent -> Networking IPC, and the security model assumption is that MESSAGE_CHECK failures mean the sender violated an invariant it was responsible for. Before the fix that assumption was false for firstPartyForCookies on this message, so ordinary application behavior was misread as compromise and terminated the WebContent process. The concrete pre-fix consequence is availability loss attributable to a legitimate embedder API call, not cross-origin data access: the cookie-isolation policy itself was never bypassed, because the validator was unconditional for every value that reached it. The residual model change worth tracking is that the fix's fail-closed branch moves cookie safety for invalid first-party values from validated to enforced-by-flag, a different and weaker form of assurance than the validator provided.

The durable lesson is about what belongs behind a MESSAGE_CHECK at all. These macros encode "a violation here means the peer is compromised," so the checked value must be one the peer is responsible for producing correctly. This message failed that test in two ways at once: the value originated with the embedding application, outside the WebContent trust boundary entirely, and it had a legitimate unset state. The fix is the textbook repair — move authority to where it exists, keep the fail-stop validator for the case where a compromised sender lies about a value it owns, and give the legitimate-unset case a fail-closed data path instead of a fail-stop one. Note also that this is the second attempt: the earlier fix patched the symptom at the UI-process layer by backfilling mainDocumentURL from self.URL, which cannot cover a WKWebView that has never navigated. Layering a default at the wrong process boundary narrows a validator's false-positive window without eliminating it; this commit deletes that fallback and relocates the derivation.