[4] WebPasteboardProxy accepted remote frame IDs outside the copied subtree
A frame ID is a name, not a permission — the pasteboard disagreed.
Medium. Site Isolation is exactly the boundary this breaks: a compromised renderer gets another site's serialized DOM without needing a second memory-safety bug in the victim's process. It stays out of the High band because it requires that prior compromise and yields disclosure only, with no memory-corruption primitive.
Under Site Isolation, frames from different sites are hosted in separate content processes, so no single process holds the DOM for a whole page. Copying a page that spans that split therefore requires the UI process to act as an aggregator: the sending process submits archives for the frames it hosts locally and a list of identifiers for the out-of-process frames, and the UI process gathers the rest. FrameIdentifier is a process-global handle, resolvable in the UI process through a registry that spans every frame in the browser. The guarantee at stake is that a process can only cause that aggregator to reach frames it is entitled to — the subtree it is actually copying.
The angle: a compromised renderer can name any live frame in the browser as a remote subframe of its own copy operation and have the UI process serialize that frame's DOM into a pasteboard the attacker named.
WebPasteboardProxy currently does no subtree checks on the remote frame IDs it is given, which means it could cause content from an unrelated cross-site remote frame to be written to the pasteboard. Fix this by adding message checks to ensure that all remote frame IDs used within WebPasteboardProxy are descendants of the root frame ID provided.
Source/WebKit/UIProcess/Cocoa/WebPasteboardProxyCocoa.mm
LayoutTests/http/tests/ipc/resources/write-web-archive-frame-ancestry-check-iframe.html
Patch Details
The change adds an ancestry validator and wires it into both pasteboard-write entry points, then reshapes the sync reply so a rejection is observable to tests.
The new file-static validateFrameIdentifiers() defines an isInSubtree lambda that walks WebFrameProxy::parentFrame() upward looking for the root, and an isAllowed lambda that accepts an identifier if it equals the root, if WebFrameProxy::webFrame(identifier) resolves to nothing, or if the resolved frame is in the root's subtree. Every identifier reaching the pasteboard path runs through it: each key of localFrameArchives, each archive's own frameIdentifier(), each entry of the archive's subframeIdentifiers(), and every entry of remoteFrameIdentifiers. writeWebContentToPasteboard gains a MESSAGE_CHECK before createOneWebArchiveFromFrames; writeWebArchiveToPasteBoard gains a MESSAGE_CHECK_COMPLETION.
To make the rejection distinguishable, the sync reply of WriteWebArchiveToPasteBoard changes from (int64_t changeCount) to (enum:uint8_t WriteWebArchiveToPasteBoardResult result, int64_t changeCount), with a new Shared/WriteWebArchiveToPasteBoardResult.h plus .serialization.in defining Success / FailureDueToInvalidFrameIdentifiers / FailureOther; every early return in the handler and its nested completion lambdas now carries a result code, and WebPlatformStrategies::writeWebArchive destructures the two-value reply while still returning only newChangeCount. Collateral: build-system registration of the new serialization file, and a test-only hook in RemotePageProxy::RemotePageProxy calling setIgnoreInvalidMessageForTesting() under ENABLE(IPC_TESTING_API) when both the ipcTestingAPIEnabled() and ignoreInvalidMessageWhenIPCTestingAPIEnabled() preferences are set, so the layout test can send a deliberately invalid message from a site-isolated subframe process without that process being torn down.
Trusting a caller-supplied global identifier as proof of entitlement, without checking that the named object lies within the subtree the caller is authorized over.
Background
Site Isolation. Frames from different sites are hosted in separate WebContent processes; a given process holds the DOM only for its own frames, and cross-site subframes appear locally as placeholder "remote frames".
FrameIdentifier. A process-global identifier for a frame. In the UI process, WebFrameProxy::webFrame(identifier) resolves any identifier to its WebFrameProxy, and WebFrameProxy::parentFrame() walks the authoritative frame tree the UI process maintains across all WebContent processes.
LegacyWebArchive. WebKit's serialized page format — main resource plus subresources plus nested subframe archives. It records the identifier of the frame it came from (frameIdentifier()) and the identifiers of its child frames (subframeIdentifiers()), so an archive submitted over IPC carries frame identifiers inside it as well as in the message's top-level vectors.
Multi-process archive assembly. When a copy spans a site-isolated frame tree, the sending process submits archives for its own local frames plus a list of FrameIdentifiers for out-of-process frames; WebPasteboardProxy::createOneWebArchiveFromFrames gathers the remaining pieces. LegacyWebArchiveCallbackAggregator collects them via addResult() and, on destruction, calls completeFrameArchive(rootFrameIdentifier) to splice subframe archives into their parents before invoking the completion handler.
MESSAGE_CHECK / MESSAGE_CHECK_COMPLETION. WebKit's IPC-validation macros. A failed check treats the message as malformed and terminates the sending connection; the _COMPLETION variant first invokes the supplied completion expression so the sync reply is not dropped.
ENABLE(IPC_TESTING_API). A build- and preference-gated facility exposing an IPC object to JavaScript in layout tests, standing in for a compromised WebContent process; setIgnoreInvalidMessageForTesting() prevents a failed MESSAGE_CHECK from killing the test's process.
Pasteboard naming. Pasteboard writes are keyed by a caller-supplied pasteboardName string, and WebPasteboardProxy also serves the read side for WebContent processes.
Analysis
This is missing authorization on IPC-supplied identifiers — a confused-deputy / site-isolation bypass leading to cross-origin information disclosure, not a memory-safety bug.
WebContent A (127.0.0.1) WebContent B (localhost)
------------------------ ------------------------
main frame [F1] iframe [F2] (child of F1)
|
| WriteWebArchiveToPasteBoard
| root = F2, remote = [F1]
v
UI process: WebPasteboardProxy
pre-fix : webFrame(F1) resolves -> archive F1 -> pasteboard
post-fix: isInSubtree(F1, root=F2) == false -> reject
The only validation applied before this change was a non-empty pasteboard name plus a webFrame(rootFrameIdentifier) null check. Because FrameIdentifier resolves through a global registry, any identifier the sender writes into the message resolves to whatever frame owns it anywhere in the browser, regardless of which process sent the message or where that frame sits in the tree. As the diagram shows, the identifier the attacker names can be an ancestor of its declared root, a sibling, or — since nothing restricted the named frame to the same page — a frame in an entirely different tab. createOneWebArchiveFromFrames would then resolve it and merge its serialized DOM into the archive written to the caller-named pasteboard.
The path is not reachable from ordinary web content: JavaScript has no way to choose the rootFrameIdentifier or the remoteFrameIdentifiers vector — the legitimate producer, WebPlatformStrategies::writeWebArchive, derives both from the frame tree actually being copied. Reaching the vulnerable state requires the ability to emit arbitrary IPC on the WebContent→UIProcess connection; the layout test substitutes IPCTestingAPIEnabled=true for that capability.
Given that capability, the attack needs no memory grooming: send WriteWebArchiveToPasteBoard with an attacker-chosen pasteboardName, rootFrameIdentifier set to a frame the compromised process legitimately owns, an empty localFrameArchives map, and remoteFrameIdentifiers containing the identifier of a cross-site victim frame. The regression test walks exactly this shape: the top-level document at 127.0.0.1:8000 embeds a localhost:8000 iframe, which under Site Isolation lands in a different process; the iframe asks its parent for IPC.frameID[0] via postMessage, then hand-encodes the sync message as [String pbName, FrameID(myFrameID), u64(0), u64(1), FrameID(parentFrameID)]. The named remote frame is the ancestor of the declared root, so isInSubtree walks upward from the parent and never encounters the child's frameID(); isAllowed returns false and the handler now replies FailureDueToInvalidFrameIdentifiers instead of assembling a cross-process archive.
Reading the result back is gated separately — WebPasteboardProxy::accessType()/canAccessPasteboardData() depend on prior grantAccess() calls or on a page in that process having domPasteAllowed() plus javaScriptCanAccessClipboard() — so recovering the archive in-process requires satisfying that gate; the disclosure to the system pasteboard, and thence to whatever the user pastes into next, does not. Escalation beyond disclosure is not indicated: no length, offset, or buffer-size field is under attacker control, so this would not convert into a memory-corruption primitive.
This vulnerability weakens the Site Isolation trust boundary the UI process is responsible for enforcing. The assumption at stake is that a WebContent process can only cause the UI process to act on frames it legitimately hosts or that descend from the frame it is copying; before the fix, a raw FrameIdentifier from an untrusted sender was treated as sufficient entitlement to reach any frame in the browser's global registry. An attacker with code execution in one WebContent process could have the UI process serialize an unrelated cross-site frame's DOM — cross-origin content and any secrets rendered into it — without needing a second memory-safety bug in the victim's process.
Insight: this is the classic shape of a Site Isolation retrofit bug. An IPC message that predates isolation carried FrameIdentifiers as pure data; when it was extended to describe a multi-process frame tree, the identifiers silently became capabilities without acquiring the matching entitlement check. Two details of the fix are instructive. It validates identifiers nested inside the serialized payload, not just the ones spelled out in the message signature — necessary because completeFrameArchive recursively expands subframeIdentifiers() to pull in more frames. And isAllowed fails open for identifiers the UI process cannot resolve (return !frame || isInSubtree(*frame)), so the guarantee delivered is "no live, resolvable, out-of-subtree frame can be pulled in", not "only subtree frames can be named" — a weaker post-condition than the commit message's phrasing suggests, and worth keeping in mind when reasoning about teardown races.
Audit directions
- An IPC handler that resolves a caller-supplied global identifier through a process-wide registry and treats successful resolution as proof of entitlement. Narrow: grep
Source/WebKit/UIProcessforWebFrameProxy::webFrame(,WebProcessProxy::webPage(, andWebProcessProxy::webFrame(inside.messages.in-declared handlers, and for each ask whether anything ties the resolved object back toWebProcessProxy::fromConnection(connection). Wider: the same class appears wherever an identifier is a bare integer key into a global table rather than a per-connection handle — audit GPUProcess/ModelProcessRemoteXXXidentifier maps andNetworkConnectionToWebProcesshandlers for lookups keyed on sender-controlledObjectIdentifiers. Widest: possession of a name is not possession of a capability — carry this into any multi-process or multi-tenant system where routing IDs, handles, or resource IDs are namespaced globally instead of per-peer (Mojo routing IDs, gRPC services keyed on client-supplied resource IDs, cloud APIs that fetch by opaque ID before evaluating an ACL). Code-review tell: a handler taking aFrameIdentifierorWebPageProxyIdentifierparameter and dereferencing it without a nearbyMESSAGE_CHECKrelating it to the sending connection. - Validation applied to identifiers named in a message signature but not to identifiers embedded inside serialized objects the message carries, where a later expansion step walks the embedded graph. Narrow: in
WebPasteboardProxyCocoa.mmandLegacyWebArchiveCallbackAggregator.h, trace every consumer ofLegacyWebArchive::subframeIdentifiers()andframeIdentifier()and confirm each expansion point is downstream ofvalidateFrameIdentifiers. Wider: audit other IPC types whose deserialized form contains further identifiers resolved later —DragData/PasteboardWebContentpayloads,WebsitePoliciesData, and any.serialization.instruct with anObjectIdentifierfield. Widest: validate the transitive closure of the payload, not its first level — the same shape drives nested-JSON authorization bypasses, GraphQL nested-resolver ACL gaps, and archive extraction that validates the manifest but not per-entry paths. Code-review tell: aMESSAGE_CHECKon a top-level parameter sitting directly next to a container parameter whose element type has identifier-typed members. - Verify the fail-open branch.
isAllowedreturns true whenWebFrameProxy::webFrame(identifier)resolves to nothing. Narrow: determine empirically whether a WebContent process can arrange for a live, attacker-interesting frame to be temporarily unresolvable in the UI-process registry (mid-navigation, mid-swap, or duringRemotePageProxysetup/teardown), and whethercreateOneWebArchiveFromFramescan still obtain that frame's content after validation returned true — this needs runtime experimentation with site-isolated navigations, not a grep. Wider: search UIProcess validators forMESSAGE_CHECK-adjacent predicates containing!obj ||orif (!x) return true;. Widest: unknown must map to deny, not to allow — the same invariant governs path canonicalization that gives up on unresolvable paths and capability checks that skip absent subjects. Code-review tell: a lookup failure folded into the allow branch rather than the reject branch. - A UI-process operation that fans out requests to other content processes with the requesting process choosing the participants. Audit the remaining callers of
createOneWebArchiveFromFrames(both are now guarded — confirm no third caller was added), then look for the same fan-out shape elsewhere inUIProcess: a handler taking aVector<FrameIdentifier>or a collection of process/page identifiers and dispatching a message per element, since each element is an implicit read of another process's state. Wider: extend to drag-and-drop, print/snapshot, find-in-page, and accessibility paths that had to grow cross-process aggregation for Site Isolation and share the risk that the aggregation set is sender-chosen. Widest: a request that causes a trusted broker to read from third parties must be scoped by the requester's authority, not by the requester's list — the same invariant governs service meshes with client-specified upstreams and batch endpoints where the caller enumerates the resource IDs. Code-review tell: aWebFrameProxy/RemotePageProxylookup inside a loop over an IPC-supplied container.