[1] CORS bypass via unvalidated SetCORSDisablingPatterns IPC
The CORS policy took a detour through the process it was written to contain.
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
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Patch Details
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.
Background
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.
Analysis
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.
Audit directions
-
Security policy relayed through the process it constrains. The enforcement point cannot distinguish embedder configuration from forged renderer input, and no argument validation can fix it because authorization is not a property of the payload. Narrow: read the
DispatchedFrom=WebContent, DispatchedTo=Networkingblock inSource/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.inand triage every message that mutates origin or scheme policy rather than performing a load —RegisterURLSchemesAsCORSEnabled, visible immediately above the deleted entry, is the closest neighbour, and the origin-access-allow-list messages in the same file are next; for each, trace whether the value it carries was originally set by aWebPageProxy/WebProcessPoolAPI in the UIProcess. Wider: the same shape appears wherever a sandboxed tier re-publishes state it received from above — checkGPUConnectionToWebProcess.messages.inandWebProcess.messages.inhandlers for settings, sandbox extensions, or content-rule-list state the WebContent process forwards onward to a third process, and check whether any UIProcess-owned preference reaches the NetworkProcess only via connection parameters populated on the WebProcess side. Widest: the invariant is a policy decision must be attested by a component at least as privileged as its enforcement point; it holds in Chromium's browser/renderer/network-service split over Mojo, in any microservice mesh where an edge service relays an authorization claim issued upstream, and in OAuth-style token relaying. Code-review tell on each rung: a value whose only writer inside the low-privilege process is a handler that copies an inbound message straight into an outbound message, with no computation in between — that memcpy-shaped relay is the fingerprint. -
Authority-by-identifier in IPC handlers. Grep
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cppfor handlers that take aWebCore::PageIdentifier,WebCore::FrameIdentifier, orWebPageProxyIdentifierparameter and use it as a table key without aMESSAGE_CHECKthat the identifier belongs to a page hosted by this connection — the deletedsetCORSDisablingPatternskeyed security-relevant state by an unvalidatedpageIdentifier, so a compromised process could have targeted another page's entry. Wider: any RPC handler that accepts an opaque integer naming a resource and grants operations on it without checking that the caller owns that resource; look for the same shape inNetworkStorageManager,WebSWServerConnection, and the page-keyed maps inNetworkProcess.cpp. Widest: this is the ambient-authority-versus-capability distinction, applicable to any RPC or syscall surface that names objects by unguessable-but-forgeable handles (Mojo interface receivers, POSIX file descriptors passed by number in a custom protocol, tenant IDs in multi-tenant HTTP APIs). Match tell: the handler dereferences a map with a caller-supplied key and never compares the result's owner to the caller's identity. -
Asymmetric set/clear pairs after a state-propagation re-route. Verify that per-page CORS-disabling state in the NetworkProcess is now torn down when the page goes away. The pre-fix design cleared it symmetrically —
WebPage::~WebPage()emptiedm_corsDisablingPatternsand re-synchronized — and that clearing path was deleted along with the rest of the WebProcess-side code, while the newsendCORSDisablingPatternsToNetworkProcessIfNecessary()early-returns when the pattern list is empty and therefore never sends a clearing update. Trace whetherNetworkConnectionToWebProcess's page-teardown handling removes the entry (the relevant handler body is not in the supplied context), and whether a recycledPageIdentifiercould inherit it. Wider: when a refactor moves a writer to a new process, the corresponding eraser is easy to lose; audit the other state replayed inWebPageProxy::finishAttachingToWebProcess()for the same set-without-clear shape. Widest: every cross-process state publisher needs a teardown edge on the same channel as its create edge — session stores keyed by connection ID, GPU resource tables, Kubernetes finalizers. Code-review tell: a sender guarded byif (state.isEmpty()) return;— the empty case is exactly the clearing case, so an empty-guard on a publisher is almost always a missing teardown. -
Trust-boundary refactors that silently narrow functional coverage. Examine whether the new UIProcess path covers every process that can load for a page under site isolation.
sendCORSDisablingPatternsToNetworkProcessIfNecessary()sendslegacyMainFrameProcess().coreProcessIdentifier()andwebPageIDInMainFrameProcess(), andNetworkProcessProxy::getNetworkProcessConnection()populatescorsDisablingPatternsPerPagefromwebProcessProxy.mainPages()only — so cross-origin subframe processes have their ownNetworkConnectionToWebProcesswhoseNetworkOriginAccessPatternsmay not receive the entries the previous per-WebPage synchronization would have produced. Compare which connections'originAccessPatterns()are populated before and after, then check the null-connection branch for whether the parsed patterns can be retained while the connection-level pattern set is not. Wider: when a fan-out publisher is replaced by a single-target publisher, enumerate the old targets; the same question applies to any per-page state infinishAttachingToWebProcessunder site isolation. Match tell: a fix that replaces an N-sender loop with one send naming a single 'main' process, in a codebase where the page can span several processes.