69aa56247c [Site Isolation] WebPasteboardProxy should only allow access to remote frames that are descendants of the root frame being copied
Triage note: IPC ancestry check preventing a web process from naming out-of-process/cross-origin frames to write their DOM to the pasteboard, a site-isolation bypass fix.
Contents
The bug at a glance
This is a site-isolation confidentiality bypass reachable from a compromised or malicious web content process, the exact threat model site isolation exists to contain. Because WebPasteboardProxy ran in the UI process and performed no subtree/ancestry check on caller-supplied remote frame identifiers, a web process could name arbitrary out-of-process frames and cause their cross-origin DOM to be serialized into a WebArchive on the pasteboard, disclosing cross-site content. It is not a memory-corruption RCE, which caps it below critical, but it defeats a core security boundary, warranting high.
The interesting angle is that the vulnerable surface is an IPC endpoint whose input is a list of FrameIdentifiers naming frames the caller may have no relationship to. Site isolation splits a page’s frames across processes, but the pasteboard-writing IPC trusted the sender to only reference its own subtree; the fix retrofits an explicit ancestry check (validateFrameIdentifiers) plus a typed result so the web process can even distinguish rejection.
Root cause
OBSERVED: WebPasteboardProxy::writeWebArchiveToPasteBoard runs in the UI process and receives, over IPC, a rootFrameIdentifier, a HashMap of localFrameArchives keyed by FrameIdentifier, and a Vector<FrameIdentifier> remoteFrameIdentifiers. Before the patch the only guard was MESSAGE_CHECK_COMPLETION(!pasteboardName.isEmpty(), …); the frame identifiers were passed straight into createOneWebArchiveFromFrames, which walks those frames and serializes their content into a LegacyWebArchive that is then placed on the pasteboard. Nothing verified that the named frames were actually descendants of rootFrameIdentifier or otherwise belonged to the requesting page.
INFERRED: Under site isolation the WebContent process that issues WriteWebArchiveToPasteBoard only legitimately owns the frames in its own local subtree; out-of-process (remote) frames belonging to other origins are represented elsewhere and are addressable only by their global FrameIdentifier. A compromised web process (or one abusing IPCTestingAPI, as the regression test does) could therefore forge a message that lists a remote frame ID belonging to an unrelated cross-site frame — for example its own parent frame from a different origin — as a remoteFrameIdentifier or as an archive subframe. The UI process would then collect that frame’s DOM into the archive and write it to the pasteboard, from which the attacker page can read it back, yielding cross-origin data disclosure.
OBSERVED: The fix introduces a static helper validateFrameIdentifiers(rootFrameIdentifier, localFrameArchives, remoteFrameIdentifiers). It defines isInSubtree, which walks parentFrame() links from a candidate frame up to the root and returns true only if rootFrameIdentifier is an ancestor, and isAllowed, which accepts the root itself, accepts identifiers whose WebFrameProxy::webFrame lookup fails (RefPtr frame is null), and otherwise requires isInSubtree(*frame). Every key of localFrameArchives, each archive’s own frameIdentifier(), each archive subframeIdentifier(), and every entry of remoteFrameIdentifiers must pass. writeWebArchiveToPasteBoard now calls MESSAGE_CHECK_COMPLETION(validateFrameIdentifiers(…), connection, completionHandler(WriteWebArchiveToPasteBoardResult::FailureDueToInvalidFrameIdentifiers, 0)) and the sibling writeWebContentToPasteboard gains MESSAGE_CHECK(validateFrameIdentifiers(…), connection).
OBSERVED: The IPC reply type changed from a bare int64_t changeCount to a pair (WriteWebArchiveToPasteBoardResult, int64_t), a new serialized enum with values Success, FailureDueToInvalidFrameIdentifiers, FailureOther, so the failure is reportable rather than silently returning 0.
Key code
The added ancestry check in WebPasteboardProxyCocoa.mm
static bool validateFrameIdentifiers(FrameIdentifier rootFrameIdentifier, const HashMap<FrameIdentifier, Ref<WebCore::LegacyWebArchive>>& localFrameArchives, const Vector<FrameIdentifier>& remoteFrameIdentifiers)
{
auto isInSubtree = [&](WebFrameProxy& frame) {
for (RefPtr ancestor = &frame; ancestor; ancestor = ancestor->parentFrame()) {
if (ancestor->frameID() == rootFrameIdentifier)
return true;
}
return false;
};
auto isAllowed = [&](FrameIdentifier identifier) {
if (identifier == rootFrameIdentifier)
return true;
RefPtr frame = WebFrameProxy::webFrame(identifier);
return !frame || isInSubtree(*frame);
};
for (auto& [frameIdentifier, archive] : localFrameArchives) {
if (!isAllowed(frameIdentifier))
return false;
Ref protectedArchive = archive;
if (auto archiveFrameIdentifier = protectedArchive->frameIdentifier(); archiveFrameIdentifier && !isAllowed(*archiveFrameIdentifier))
return false;
for (auto subframeIdentifier : protectedArchive->subframeIdentifiers()) {
if (!isAllowed(subframeIdentifier))
return false;
}
}
for (auto identifier : remoteFrameIdentifiers) {
if (!isAllowed(identifier))
return false;
}
return true;
}
Patch walkthrough
Source/WebKit/UIProcess/Cocoa/WebPasteboardProxyCocoa.mm— Adds the static validateFrameIdentifiers() helper enforcing the subtree/ancestry rule, and wires it into both writeWebContentToPasteboard (MESSAGE_CHECK) and writeWebArchiveToPasteBoard (MESSAGE_CHECK_COMPLETION returning FailureDueToInvalidFrameIdentifiers). writeWebArchiveToPasteBoard’s completion handler is rewritten to thread the new WriteWebArchiveToPasteBoardResult through every early-return and the success path.Source/WebKit/Shared/WriteWebArchiveToPasteBoardResult.h— New header defining enum class WriteWebArchiveToPasteBoardResult : uint8_t { Success, FailureDueToInvalidFrameIdentifiers, FailureOther } so the UI process can report why the write was rejected.Source/WebKit/Shared/WriteWebArchiveToPasteBoardResult.serialization.in— Serialization description registering the new enum for IPC so it can be sent in the reply tuple.Source/WebKit/UIProcess/WebPasteboardProxy.messages.in— Changes the WriteWebArchiveToPasteBoard reply from (int64_t changeCount) to (enum:uint8_t WebKit::WriteWebArchiveToPasteBoardResult result, int64_t changeCount).Source/WebKit/UIProcess/WebPasteboardProxy.h— Forward-declares the enum and updates the writeWebArchiveToPasteBoard declaration’s CompletionHandler signature to void(WriteWebArchiveToPasteBoardResult, int64_t).Source/WebKit/WebProcess/WebCoreSupport/WebPlatformStrategies.cpp— Web-process caller updated to destructure the new two-element reply: auto [result, newChangeCount] = sendResult.takeReplyOr(WriteWebArchiveToPasteBoardResult::FailureOther, 0);Source/WebKit/UIProcess/RemotePageProxy.cpp— Under ENABLE(IPC_TESTING_API), propagates setIgnoreInvalidMessageForTesting() to remote page processes when the test preferences are set, so the layout test’s forged cross-process message is observable rather than immediately fatal.LayoutTests/http/tests/ipc/write-web-archive-frame-ancestry-check.html— Regression test (plus its iframe resource and expected output) that forges a WriteWebArchiveToPasteBoard naming the out-of-process parent frame as a remote subframe and asserts the reply is FailureDueToInvalidFrameIdentifiers.
Background
Site Isolation — WebKit’s site-isolation model places frames from different sites into separate WebContent processes so that a compromise of one process cannot read another site’s DOM or resources. Cross-process frames are called remote frames and are referenced by a global FrameIdentifier. Any UI-process IPC that accepts frame identifiers from a web process must independently verify the caller is entitled to those frames, because the sender is treated as potentially adversarial.
WebArchive / LegacyWebArchive — A WebArchive is a serialized snapshot of a frame and its subframes, including their DOM and subresources, written to the pasteboard as WebArchivePboardType when a user copies rich content. createOneWebArchiveFromFrames assembles one from a root frame plus supplied local archives and remote frame identifiers. If an attacker can steer which frames are included, the archive becomes a vehicle to exfiltrate cross-origin content.
WebPasteboardProxy — The UI-process singleton receiving pasteboard IPC from web processes. It holds authority the web process lacks (access to the real pasteboard and to all frame proxies), so it is a classic confused-deputy surface: it must validate every identifier a web process names rather than assuming good faith.
MESSAGE_CHECK / MESSAGE_CHECK_COMPLETION — WebKit IPC validation macros. On failure they treat the message as malformed, typically terminating the offending web process (and, with MESSAGE_CHECK_COMPLETION, invoking the completion handler with a supplied default). They are the standard mechanism for enforcing invariants on attacker-controlled IPC arguments; the absence of one here is precisely the bug.
IPCTestingAPI — A test-only facility that lets layout tests craft and send raw IPC messages (window.IPC) to exercise message-check paths. The regression test uses it under SiteIsolationEnabled to forge a WriteWebArchiveToPasteBoard that names its cross-process parent frame, verifying the new check returns FailureDueToInvalidFrameIdentifiers.
Vulnerability window
- Design — WriteWebArchiveToPasteBoard is defined to accept a root frame plus local archives and remote frame identifiers, with the only IPC guard being a non-empty pasteboard name check.
- Site isolation introduced — Cross-origin frames move into separate processes and become addressable by global FrameIdentifier, but the pasteboard IPC continues to trust the caller’s list of frame IDs without an ancestry check.
- Gap — A compromised or malicious web process can name remote frames outside its own subtree (e.g. an unrelated cross-site frame) and have their DOM serialized into a pasteboard WebArchive it can then read back.
- Report — Tracked as bugs.webkit.org 315692 / rdar://176891998, framed as WebPasteboardProxy doing no subtree checks on remote frame IDs.
- Fix — validateFrameIdentifiers enforces that every referenced frame descends from the root; MESSAGE_CHECK(_COMPLETION) rejects violations, and a typed WriteWebArchiveToPasteBoardResult reports the rejection.
- Regression test — A layout test forges a cross-process frame reference via IPCTestingAPI and asserts FailureDueToInvalidFrameIdentifiers, locking in the boundary.
Proof of concept
The added layout test (write-web-archive-frame-ancestry-check-iframe.html) uses IPCTestingAPI under SiteIsolationEnabled. The child iframe learns its cross-process parent’s FrameID, then forges a WriteWebArchiveToPasteBoard synchronous IPC that lists the parent frame as a remote subframe (remoteFrameIdentifiers count 1, the parent’s ID). Pre-patch this would collect the parent’s cross-origin DOM into the pasteboard archive; post-patch validateFrameIdentifiers returns false and the reply’s result field is FailureDueToInvalidFrameIdentifiers (value 1), which the harness asserts.
const u64 = (v) => ({ type: 'uint64_t', value: v });
const S = (v) => ({ type: 'String', value: v });
const FrameID = (v) => ({ type: 'FrameID', value: [v] });
function finish(msg) { parent.postMessage(msg, '*'); }
async function go() {
if (!window.IPC || !IPC.messages.WebPasteboardProxy_WriteWebArchiveToPasteBoard) {
finish({ skipped: true });
return;
}
let myFrameID = IPC.frameID[0];
let parentFrameID = await new Promise((resolve) => {
addEventListener('message', (e) => resolve(BigInt(e.data.parentFrameID)), { once: true });
parent.postMessage({ getParentFrameID: true }, '*');
});
let pbName = 'com.apple.WebKit.TestWebArchiveAncestry.' + Date.now();
// Forge a WriteWebArchiveToPasteBoard IPC that names an out-of-process frame ID (the parent
// frame) as a remote subframe. This should fail with the FailureDueToInvalidFrameIdentifiers
// error to prevent writing a cross-process DOM to the pasteboard.
let result = null;
try {
let r = IPC.sendSyncMessage('UI', 0, IPC.messages.WebPasteboardProxy_WriteWebArchiveToPasteBoard.name, 1000, [
S(pbName),
FrameID(myFrameID),
u64(0n),
u64(1n), FrameID(parentFrameID),
]);
if (r && r.arguments && r.arguments.length >= 1 && r.arguments[0])
result = Number(r.arguments[0].value);
} catch (e) { }
finish({ result });
}
addEventListener('load', () => setTimeout(go, 0));
Exploitation
- Prerequisite — Attacker needs code execution in a WebContent process, or the ability to send crafted IPC (the test path uses IPCTestingAPI, which is not present in shipping builds). In practice this is a post-compromise capability leveraged after an initial renderer bug.
- Frame targeting — The attacker enumerates or guesses the global FrameIdentifier of a cross-site out-of-process frame it wants to read (e.g. an embedded frame of another origin, or its own remote parent) and lists it among localFrameArchives keys/subframes or remoteFrameIdentifiers.
- Trigger — It sends WriteWebArchiveToPasteBoard naming that frame; the UI process serializes the frame’s DOM into a WebArchive on the named pasteboard.
- Exfiltration — The attacker reads the WebArchive back from the pasteboard it controls, recovering cross-origin DOM content. This is an information-disclosure primitive, not memory corruption; there is no code execution from this bug alone.
Detection & hunting
For defenders and SOC / detection engineers:
- IPC frame-identifier mismatch —
- WebContent termination on pasteboard write —
- Anomalous WebArchive pasteboard writes —
Audit directions
- Other WebPasteboardProxy endpoints —
- createOneWebArchiveFromFrames callers —
- Cross-process FrameIdentifier trust —
- Silent-default completions —