← All reports

[1] CORS bypass via unvalidated SetCORSDisablingPatterns IPC

HighWebKit multi-process IPC layer (NetworkProcess)CrossOrigin

The CORS policy took a detour through the process it was written to contain.

841ad59

High. The policy that tells the network layer when to skip CORS travelled to its enforcement point through the sandboxed process that policy exists to constrain. No payload validation could have closed it — the routing itself was the bug, and a renderer with code execution had a one-message path to universal credentialed reads.

WebKit splits work between a trusted UIProcess, sandboxed WebContent processes that run untrusted script, and a NetworkProcess that owns the cookie jar and performs the actual HTTP loads. Embedders can hand WebKit a per-page list of URL match patterns — the _corsDisablingPatterns setting — for which CORS enforcement is skipped, and that list must reach both the renderer (for WebCore-side same-origin checks) and the network layer (which decides whether to reject a credentialed cross-origin response). The security expectation is that a value governing enforcement in the NetworkProcess is attested only by a process at least as privileged as that enforcement point.

The angle: a renderer that already holds code execution can send one message declaring *://*/* and have the network layer stop enforcing CORS for every URL, turning renderer compromise into a universal read of every site the user is logged into.

The commit message states the pre-fix path plainly: _corsDisablingPatterns flowed from the UIProcess through the WebContent process to the NetworkProcess via Messages::NetworkConnectionToWebProcess::SetCORSDisablingPatterns, and a compromised WebContent process could send that IPC with attacker-chosen patterns to disable CORS for arbitrary cross-origin URLs and read the content of any site the user was authenticated to. The patch routes the patterns directly from the UIProcess to the NetworkProcess, removing the WebContent process from the trust path, while keeping Messages::WebPage::UpdateCORSDisablingPatterns so the renderer can still populate its own in-process origin-access patterns.

Source/WebKit/NetworkProcess/NetworkProcess.cpp

@@ NetworkProcess::createNetworkConnectionToWebProcess
+ // Apply CORS-disabling patterns supplied by the UIProcess at connection-creation time. This covers the case
+ // where _corsDisablingPatterns was set on a WebPageProxy before the NetworkProcess was launched, so no
+ // SetCORSDisablingPatternsForPage IPC could reach this process.
+ for (auto& [pageIdentifier, patterns] : parameters.corsDisablingPatternsPerPage)
+ setCORSDisablingPatternsForPage(identifier, pageIdentifier, WTF::move(patterns));
...
-void NetworkProcess::setCORSDisablingPatterns(NetworkConnectionToWebProcess& connection, PageIdentifier pageIdentifier, Vector<String>&& patterns)
+void NetworkProcess::setCORSDisablingPatternsForPage(WebCore::ProcessIdentifier webProcessIdentifier, PageIdentifier pageIdentifier, Vector<String>&& patterns)
{
+ // This message is sent directly from the UIProcess rather than from the WebProcess because a compromised
+ // WebContent process must not be able to disable CORS on the NetworkProcess side; that would let it read
+ // the content of arbitrary cross-origin sites.
auto parsedPatterns = WTF::compactMap(WTF::move(patterns), [&](auto&& pattern) -> std::optional<UserContentURLPattern> {
UserContentURLPattern parsedPattern(WTF::move(pattern));
- if (parsedPattern.isValid()) {
- connection.originAccessPatterns().allowAccessTo(parsedPattern);
- return parsedPattern;
- }
- return std::nullopt;
+ if (!parsedPattern.isValid())
+ return std::nullopt;
+ if (RefPtr connection = webProcessConnection(webProcessIdentifier))
+ connection->originAccessPatterns().allowAccessTo(parsedPattern);
+ return parsedPattern;
});

Source/WebKit/WebProcess/WebPage/WebPage.cpp

@@ WebPage::updateCORSDisablingPatterns
m_corsDisablingPatterns = WTF::move(patterns);
- synchronizeCORSDisablingPatternsWithNetworkProcess();
page->setCORSDisablingPatterns(parseAndAllowAccessToCORSDisablingPatterns(m_corsDisablingPatterns));
}
 
-void WebPage::synchronizeCORSDisablingPatternsWithNetworkProcess()
-{
- // FIXME: We should probably have this mechanism done between UIProcess and NetworkProcess directly.
- WebProcess::singleton().ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::SetCORSDisablingPatterns(m_identifier, m_corsDisablingPatterns), 0);
-}

The change removes the WebContent-to-Networking delivery path, adds a UIProcess-to-Networking one, and carries the pattern list through connection-creation parameters for the case where no NetworkProcess exists yet.

Removed: the SetCORSDisablingPatterns(WebCore::PageIdentifier, Vector<String>) entry in NetworkConnectionToWebProcess.messages.in (a receiver block annotated DispatchedFrom=WebContent, DispatchedTo=Networking), its handler NetworkConnectionToWebProcess::setCORSDisablingPatterns and declaration, WebPage::synchronizeCORSDisablingPatternsWithNetworkProcess() and its three call sites (the WebPage constructor, WebPage::~WebPage, WebPage::updateCORSDisablingPatterns), and the loop in WebProcess::ensureNetworkProcessConnection() that re-published every live WebPage's own m_corsDisablingPatterns onto each newly created network connection.

Added: SetCORSDisablingPatternsForPage(WebCore::ProcessIdentifier, WebCore::PageIdentifier, Vector<String>) in NetworkProcess.messages.in. NetworkProcess::setCORSDisablingPatterns(NetworkConnectionToWebProcess&, ...) is renamed to setCORSDisablingPatternsForPage(ProcessIdentifier, PageIdentifier, Vector<String>&&) and now resolves the connection itself through webProcessConnection(webProcessIdentifier) — a null-tolerant RefPtr — instead of receiving it from the dispatch machinery. WebPageProxy::sendCORSDisablingPatternsToNetworkProcessIfNecessary() is added and called from both WebPageProxy::setCORSDisablingPatterns() and WebPageProxy::finishAttachingToWebProcess(), the latter covering process swaps, sending legacyMainFrameProcess().coreProcessIdentifier() and webPageIDInMainFrameProcess().

For the pre-launch case, a HashMap<WebCore::PageIdentifier, Vector<String>> corsDisablingPatternsPerPage field is added to NetworkProcessConnectionParameters (header plus .serialization.in), populated in NetworkProcessProxy::getNetworkProcessConnection() from page->corsDisablingPatterns() and applied in NetworkProcess::createNetworkConnectionToWebProcess().

Security policy delivered to its enforcement point by routing it through the very component the policy constrains, so compromising that component forges the policy.

Where this lives. WebKit's IPC layer carries messages between the UIProcess (the trusted application process), one or more sandboxed WebContent processes, and the NetworkProcess. WebContent is treated as fully attacker-controlled once a renderer bug is exploited; the UIProcess is trusted; the NetworkProcess holds the cookie jar and credentials and performs all real network I/O.

Message declaration and annotation. IPC endpoints are declared in .messages.in files. A receiver block can carry annotations such as DispatchedFrom=WebContent, DispatchedTo=Networking, which state which process may send the messages in that block. MESSAGE_CHECK macros are WebKit's convention for validating IPC arguments in a receiver and killing the sender's connection on violation.

Connection objects. NetworkConnectionToWebProcess is the NetworkProcess-side object representing one WebContent process's connection; NetworkProcess::webProcessConnection(WebCore::ProcessIdentifier) looks one up. NetworkProcessConnectionParameters is the struct the UIProcess fills in via NetworkProcessProxy::getNetworkProcessConnection() and hands to NetworkProcess::createNetworkConnectionToWebProcess() when a WebProcess's network connection is established.

CORS-disabling patterns. These are per-page embedder configuration surfaced as _corsDisablingPatterns and reaching WebKit through WebPageProxy::setCORSDisablingPatterns. Each string is parsed into a WebCore::UserContentURLPattern, whose isValid() reports only whether the string parsed as a well-formed pattern — *://*/* matches any scheme, host and path. On the network side the parsed patterns are consulted by NetworkProcess::shouldDisableCORSForRequestTo(PageIdentifier, URL) and registered in the connection's NetworkOriginAccessPatterns; on the WebCore side the equivalent list is installed via Page::setCORSDisablingPatterns().

Identifiers. WebCore::PageIdentifier names a page inside a WebContent process and WebCore::ProcessIdentifier names a WebContent process. Both are plain ObjectIdentifier integers carried in IPC payloads.

Process swap. A page may be moved to a different WebContent process during navigation; WebPageProxy::finishAttachingToWebProcess() runs in the UIProcess after the page attaches to a possibly-new WebProcess and is where per-page state is re-established.

The pattern list originates as trusted embedder configuration in the UIProcess but reached the NetworkProcess only by transiting the renderer:

  Before:                                    After:
  UIProcess                                  UIProcess
    └─► WebPage::UpdateCORSDisablingPatterns    ├─► WebPage::UpdateCORSDisablingPatterns
          │  (WebContent)                       │     (WebContent, in-process checks only)
          └─► synchronizeCORSDisabling...()     └─► NetworkProcess::SetCORS...ForPage
                └─► NetworkConnectionToWebProcess              │
                    ::SetCORSDisablingPatterns   ◄── forgeable └─► enforcement state
                          └─► enforcement state

The handler NetworkConnectionToWebProcess::setCORSDisablingPatterns forwarded its arguments straight to NetworkProcess::setCORSDisablingPatterns with no MESSAGE_CHECK of any kind — neither on the pattern strings (only UserContentURLPattern::isValid() was consulted, a syntax check rather than an authorization check) nor on whether the supplied PageIdentifier belonged to a page hosted by that connection. The missing invariant is that a value governing whether the NetworkProcess skips CORS enforcement must be attested by a process at least as privileged as the enforcement point; relaying it through the sandboxed renderer made the renderer's word authoritative over a policy that exists to constrain the renderer.

This is not reachable from web content directly — the endpoint is an IPC message, so an attacker must first hold arbitrary code execution (or at least arbitrary IPC-send capability) inside a WebContent process via a separate renderer bug. Given that, the trigger is a single unauthenticated write on the existing network connection: SetCORSDisablingPatterns(pageID, { "*://*/*" }). Because that string is syntactically well-formed, isValid() returns true, the pattern is registered via connection.originAccessPatterns().allowAccessTo(parsedPattern), and it is retained for later consultation by shouldDisableCORSForRequestTo(pageIdentifier, url) — the retention line sits just past the end of the shown hunk, but the page-keyed signature directly above it shows the lookup is per-page. The attacker then issues ordinary credentialed loads (fetch(url, {credentials:'include'}), XMLHttpRequest, or synthesized ScheduleResourceLoad IPC) to any origin, and the network-side cross-origin check is skipped, returning the response body to the compromised renderer. The renderer-side WebCore check is no obstacle: a compromised process controls its own address space and holds the UpdateCORSDisablingPatterns handler locally.

Two properties sharpened the primitive. The pageIdentifier argument was never validated as belonging to a page hosted by the sending connection, so an attacker could target an identifier other than its own — though whether the NetworkProcess keys the entry purely by PageIdentifier rather than per-connection is not established by the supplied context. And the deleted loop in WebProcess::ensureNetworkProcessConnection() re-published each live WebPage's own m_corsDisablingPatterns onto every newly created network connection, so an attacker who also overwrote that member in its own address space would have the permissive values re-applied automatically across connection teardown rather than needing to resend the forged message.

This change expands the attack surface of the NetworkProcess by adding a new IPC entry point, Messages::NetworkProcess::SetCORSDisablingPatternsForPage, plus a new serialized corsDisablingPatternsPerPage field in NetworkProcessConnectionParameters. The NetworkProcess's CORS-disabling state can now be written by a message on the NetworkProcess receiver and at connection-creation time, and the handler performs a connection lookup using an identifier carried in the message rather than using the dispatching connection. Two things are assumed rather than enforced: that the NetworkProcess receiver block is only dispatchable over the parent connection — the block header visible in the diff, messages -> NetworkProcess : AuxiliaryProcess WantsAsyncDispatchMessage {, carries no DispatchedFrom= annotation in the supplied excerpt, so this rests on the general WebKit process-model expectation — and that the webProcessIdentifier argument is honest, since the handler does not cross-check it against the sending connection. NetworkProcessConnectionParameters is authored in the UIProcess, so the same trust assumption applies there. If either assumption broke, the pre-fix bypass would reappear in a new shape, with the added twist that the attacker would also choose which connection's originAccessPatterns gets the permissive entry. Newly reachable too: setCORSDisablingPatternsForPage may run when webProcessConnection() returns null, retaining the parsed patterns while the connection-level pattern set is not updated — a state combination that could not occur when the connection was supplied by the dispatch machinery.

This vulnerability weakens the WebContent-process sandbox boundary and, through it, the same-origin policy enforced at the network layer. The security model assumes a compromised renderer is confined by policy state held in a more-privileged process; before the fix that assumption did not hold, and an attacker with renderer code execution could disable CORS for arbitrary URLs and read the bodies of credentialed cross-origin responses — a universal read of every site the user is authenticated to (webmail, banking, internal corporate apps) without any further sandbox escape.

The deleted helper carried its own tombstone: // FIXME: We should probably have this mechanism done between UIProcess and NetworkProcess directly. The insecure routing was documented as suboptimal long before it was recognized as a boundary violation. The general shape is policy laundering: a value trustworthy at its origin becomes untrustworthy the moment it is relayed through a lower-privilege process, yet the receiving code's mental model still treats it as embedder configuration. Because it looks like configuration rather than attacker input, no MESSAGE_CHECK was ever written for it — and none could have been, since "is this pattern authorized?" is unanswerable at the network layer without attestation from a trusted process. The only correct fix is a routing change. Note that NetworkConnectionToWebProcess.messages.in still contains at least one neighbour with the same origin-policy-mutating character in the same DispatchedFrom=WebContent block — RegisterURLSchemesAsCORSEnabled sits directly above the deleted line.