← WebKit Silent-Fix Report — 2026-W21

073bf48042  [Site Isolation] Handle RemoteFrames in AuthenticatorCoordinator::scopeAndCrossOriginParent

severity medium class CrossOrigin confidence 0.62 WebAuthn / Site Isolation exploitable-grade
Anthony Tarbinian Mon May 18 09:48:44 2026 -0700 full: 073bf48042ce0c1bc752e744fd7252dfc5319399 bug report ↗ view on GitHub ↗
Primitive: same-origin-with-ancestors check bypass across RemoteFrames
Triage note: Rewrites the same-origin-domain ancestor walk to use frame tree/RemoteFrame origins, fixing a WebAuthn origin-scoping check that was broken for cross-process iframes.
Contents

The bug at a glance

This is a security-check correctness fix in the WebAuthn ancestor-origin scoping path, not a memory-corruption bug, so its ceiling is a logic/spoofing class rather than RCE. Under Site Isolation the pre-patch loop walked Document::parentDocument(), which returns null the moment an ancestor lives in another process; the same-site/cross-origin determination therefore ran against a truncated ancestor chain and could reach the wrong verdict for a credential ceremony. Because the observable failure mode in the shipped configuration was a crash (the WPT was marked [ Crash ]/[ Failure ] on the site-isolation bots), the realistic impact is a reliability defect plus a latent scoping-integrity concern for cross-origin embedded WebAuthn. It is rated high because a mis-scoped same-origin-with-ancestors result touches the trust boundary of public-key credential creation/assertion.

WebAuthn’s cross-origin ancestry check was written for the classic single-process document tree and silently degraded once Site Isolation put ancestor frames in other processes. The interesting part is that the fix has to reconstruct a security decision (same-origin, same-site, or cross-origin parent) using only what a RemoteFrame can expose across the process boundary — a SecurityOrigin and its registrable domain — because the full ancestor URL is no longer reachable.

Root cause

AuthenticatorCoordinatorInternal::scopeAndCrossOriginParent computes two facts about the frame requesting a credential operation: whether every ancestor is same-site (isSameSite) and, if any ancestor is cross-origin, the SecurityOriginData of the first such crossOriginParent. These feed the WebAuthn scope and the crossOriginParent reported to the authenticator, i.e. the same-origin-with-ancestors gating that decides whether an embedded frame may drive a ceremony at all.

Before the patch the traversal was document-centric: for (RefPtr parentDocument = document.parentDocument(); parentDocument; parentDocument = parentDocument->parentDocument()). Document::parentDocument() only yields a Document when the parent frame is a LocalFrame in the same process. With Site Isolation, a cross-origin ancestor is represented locally by a RemoteFrame whose Document does not exist in this process, so parentDocument() returns null and the loop terminates early — before reaching the true top ancestors. Every ancestor beyond the first out-of-process boundary is therefore invisible, so isSameSite can stay true and crossOriginParent can stay unset even when a genuinely cross-origin/cross-site ancestor exists higher up. The check also compared against parentDocument->url() via areRegistrableDomainsEqual, another value unavailable for a remote ancestor.

The patch re-bases the walk on the frame tree, which is fully materialized in every process under Site Isolation: for (RefPtr parentFrame = document.frame() ? document.frame()->tree().parent() : nullptr; parentFrame; parentFrame = parentFrame->tree().parent()). For each ancestor frame it obtains parentFrame->frameDocumentSecurityOrigin(), a value proxied for RemoteFrames. The same-site test becomes if (!parentOrigin || is<RemoteFrame>(parentFrame) || RegistrableDomain(parentOrigin->data()) != RegistrableDomain(origin->data())) isSameSite = false;. Two deliberate hardening choices are visible here: a missing origin conservatively fails same-site, and any RemoteFrame ancestor is treated as not-same-site because the process cannot fully prove same-site with only cross-process data — the registrable-domain comparison is kept as the mechanism but the RemoteFrame case is forced to the safe answer.

The cross-origin-parent capture is likewise rewritten to use the proxied origin: if (parentOrigin && !origin->isSameOriginAs(*parentOrigin)) crossOriginParent = parentOrigin->data();, replacing the old isSameOriginDomain(…) plus areRegistrableDomainsEqual(url, parentDocument->url()) pair that depended on the now-absent parent Document and URL. Net effect: the ceremony’s scope is computed over the complete ancestor chain again, and the remote-ancestor cases fail closed rather than defaulting to same-origin/same-site.

Key code

AuthenticatorCoordinator.cpp — frame-tree ancestor walk with RemoteFrame handling

    for (RefPtr parentFrame = document.frame() ? document.frame()->tree().parent() : nullptr; parentFrame; parentFrame = parentFrame->tree().parent()) {
        RefPtr parentOrigin = parentFrame->frameDocumentSecurityOrigin();
        if (!parentOrigin || is<RemoteFrame>(parentFrame) || RegistrableDomain(parentOrigin->data()) != RegistrableDomain(origin->data()))
            isSameSite = false;
        if (parentOrigin && !origin->isSameOriginAs(*parentOrigin))
            crossOriginParent = parentOrigin->data();
    }

Patch walkthrough

  • Source/WebCore/Modules/webauthn/AuthenticatorCoordinator.cpp — scopeAndCrossOriginParent’s ancestor loop is converted from Document::parentDocument() iteration to FrameTree::parent() iteration so it no longer terminates at the first out-of-process boundary. Each ancestor’s SecurityOrigin is fetched via frameDocumentSecurityOrigin(); same-site now fails closed when the origin is absent or the ancestor is a RemoteFrame, and registrable-domain equality is computed with RegistrableDomain(origin->data()) rather than areRegistrableDomainsEqual on URLs. crossOriginParent is captured from the proxied origin. New includes RemoteFrame.h and LocalFrameInlines.h support the is<RemoteFrame> test and frame-tree access.
  • LayoutTests/platform/ios-site-isolation/TestExpectations — Removes the [ Crash ] expectation for http/wpt/webauthn/public-key-credential-same-origin-with-ancestors.https.html, confirming the pre-patch path crashed under iOS site isolation and now passes.
  • LayoutTests/platform/mac-site-isolation/TestExpectations — Removes the [ Failure ] expectation for the same WPT on mac site isolation, corroborating the fix restores correct same-origin-with-ancestors behavior.

Background

Site Isolation: LocalFrame vs RemoteFrame — Under Site Isolation WebKit splits a page’s frame tree across multiple web processes by origin/site. In a given process, frames hosted in that process are LocalFrames with real Documents, while frames belonging to other processes are represented by RemoteFrame placeholders. A RemoteFrame carries proxied metadata (such as a SecurityOrigin) but has no live Document in the current process, so any code that reaches for the ancestor’s Document, URL, or DOM state gets nullptr. Correct security code under Site Isolation must therefore walk the frame tree, which is fully mirrored in every process, rather than the document tree.

Document::parentDocument() truncation — parentDocument() returns the parent frame’s Document only when that parent is local. When the parent is a RemoteFrame it returns null. Any loop of the form for (doc = document.parentDocument(); doc; doc = doc->parentDocument()) therefore stops at the first cross-process boundary, silently omitting all higher ancestors from whatever aggregate it is computing — here, the same-site and cross-origin-parent facts.

WebAuthn ancestor-origin scoping (same-origin-with-ancestors) — The Web Authentication API restricts credential creation and assertion based on the requesting frame’s relationship to its ancestors. A ceremony run inside an embedded frame must know whether all ancestors are same-origin, whether the chain is same-site, and which ancestor (if any) is the first cross-origin parent; these values gate whether the operation is permitted and how it is reported to the authenticator. Getting the ancestor set wrong changes the trust decision for a credential operation, so the computation must see the entire chain.

RegistrableDomain and same-site vs same-origin — RegistrableDomain (the eTLD+1) is the unit of the same-site check, distinct from same-origin (scheme+host+port). The pre-patch code mixed a URL-based areRegistrableDomainsEqual with an isSameOriginDomain call on Documents; the patch standardizes on RegistrableDomain(origin->data()) so the comparison works from a SecurityOriginData alone, which is all a RemoteFrame can supply across the process boundary.

Fail-closed defaults for cross-process unknowns — When a security determination cannot be fully proven with the data available in-process, the safe default is the more restrictive verdict. The patch encodes this by forcing isSameSite = false whenever the ancestor origin is missing or the ancestor is a RemoteFrame, and only setting crossOriginParent when a real origin proves a cross-origin relationship — rather than treating an unreachable ancestor as implicitly same-origin/same-site.

Vulnerability window

  1. Original design — scopeAndCrossOriginParent walks Document::parentDocument() and uses isSameOriginDomain plus areRegistrableDomainsEqual(url, parentDocument->url()) — correct in a single-process frame tree where every ancestor Document is reachable.
  2. Site Isolation lands — Cross-origin ancestors become RemoteFrames without in-process Documents; parentDocument() begins returning null at the first process boundary, truncating the ancestor walk.
  3. Latent defect — For any embedded WebAuthn frame with an out-of-process ancestor, the scope is computed over an incomplete chain, and the reachable code path crashes or mis-decides on the site-isolation bots (WPT marked Crash/Failure).
  4. Diagnosis (bug 314439 / rdar://176593716) — The traversal is identified as document-centric and incompatible with Site Isolation’s cross-process ancestors.
  5. Fix — The walk is re-based on FrameTree::parent(); origins come from frameDocumentSecurityOrigin(); same-site fails closed for missing origins and RemoteFrames; cross-origin parent is captured from proxied origin data.
  6. Validation — http/wpt/webauthn/public-key-credential-same-origin-with-ancestors.https.html is de-listed from both ios- and mac-site-isolation TestExpectations, confirming correct behavior under Site Isolation.

Triggering

No standalone PoC is added; the patch only removes TestExpectations lines for the existing WPT public-key-credential-same-origin-with-ancestors.https.html. Trigger: with Site Isolation enabled, embed a WebAuthn-invoking document inside a cross-origin/cross-site ancestor chain (so at least one ancestor is a RemoteFrame in another process) and invoke navigator.credentials.create()/get() from the innermost frame; pre-patch the ancestor walk truncates at the process boundary and the same-origin-with-ancestors WPT crashes/fails.

Exploitation

  1. Setup — Construct a nested frame hierarchy that Site Isolation splits across processes — e.g. a.example embeds b.other which embeds a WebAuthn-using document — so the requesting frame has a RemoteFrame ancestor.
  2. Trigger — Run a credential ceremony from the innermost frame. Pre-patch, scopeAndCrossOriginParent evaluates isSameSite/crossOriginParent over a truncated chain, yielding a scope determination that ignores ancestors beyond the first process boundary.
  3. Impact ceiling — Observed outcome in the shipped configuration is crash-only (the WPT was expected to Crash/Fail). The latent integrity concern is a mis-scoped ceremony (ancestors wrongly judged same-site/same-origin), but no memory-corruption primitive is present; this is a logic/reliability fix, not a corruption exploit.

Detection & hunting

For defenders and SOC / detection engineers:

  • WebAuthn ceremonies from deeply nested cross-origin frames — In telemetry, flag navigator.credentials create/get calls originating from frames with cross-process ancestors on Site-Isolation builds; a spike of ceremonies whose reported crossOriginParent is empty despite a cross-origin embedding chain is anomalous.
  • Renderer crashes in AuthenticatorCoordinator — Watch web-process crash reports with frames in scopeAndCrossOriginParent / parentDocument near WebAuthn credential entry points on affected versions; these correspond to the pre-patch truncated-walk path.

Audit directions

  • Other Document::parentDocument() consumers — Grep WebCore for security or scoping loops built on parentDocument()/ownerElement()/document tree walks that must run under Site Isolation; each is a candidate for the same truncation and should be re-based on FrameTree::parent().
  • SecurityOrigin availability on RemoteFrames — Audit callers of frameDocumentSecurityOrigin() and related proxied getters to confirm they handle a null/opaque origin by failing closed rather than defaulting to permissive.
  • Ancestor-based policy checks generally — Review other ancestor-chain security gates (Permissions Policy, feature policy, Storage Access, mixed content) for the same document-vs-frame-tree assumption under Site Isolation.
  • RegistrableDomain vs isSameOriginDomain consistency — Ensure same-site checks elsewhere in WebAuthn and credential management consistently use SecurityOriginData-derived RegistrableDomain so they remain valid when only proxied origin data exists.

Before / after

Loading diff…