[1] WebContent-supplied origin forwarded to system services without UI-process re-derivation
The UI process asked the sandbox which site it was talking to.
High. Four independent handlers in the privileged process used a site identity that arrived from inside the sandbox as the key to a permission or platform-service decision. Escalation past identity spoofing depends on what each system service does with the value, but the geolocation path alone puts the wrong site's name on a prompt the user is asked to approve.
WebKit splits a browsing session between a sandboxed WebContent process that parses and executes site content and a UI process that owns navigation state, renders permission prompts, and acts as the client of platform services such as AppSSO, MarketplaceKit, and CoreLocation. The UI process keeps its own mirror of each frame — a WebFrameProxy, whose url() and securityOrigin() are recorded when the document commits — while WebContent attaches its own serializable FrameInfoData frame description to many of the messages it sends. The expectation is that any site identity the UI process hands onward reflects the document it actually committed.
The angle: a compromised WebContent process can put a victim site's name on a geolocation prompt, an AppSSO authorization, and a MarketplaceKit install, and can redeem a location grant under any registrable domain it names.
Four UI-process IPC handlers accepted an origin/domain field from a DispatchedFrom=WebContent message and forwarded it verbatim to a system service as a per-site authorization key, without comparing it against UI-process-authoritative state. A compromised WebContent process could spoof the origin and have CoreLocation, AppSSO, MarketplaceKit, or the geolocation policy decider apply another site's permission decision. The change re-derives each value from UI-process-authoritative state instead of trusting the IPC-supplied field.
Source/WebKit/UIProcess/WebPageProxy.cpp
Source/WebKit/UIProcess/WebGeolocationManagerProxy.cpp
Source/WebKit/UIProcess/Cocoa/SOAuthorization/SOAuthorizationSession.mm
LayoutTests/ipc/forged-geolocation-permission-frame-origin.html
Patch Details
The change re-derives three forwarded origin values from UI-process state at their consuming call sites, and separately gives the geolocation authorization token a scope it can be checked against at redemption.
In the origin-re-derivation cluster: SOAuthorizationSession drops the read of m_navigationAction->sourceFrame()->securityOrigin() entirely and computes initiatorOrigin as SecurityOrigin::create(mainFrame->url())->toString() for all initiating actions, guarded by !mainFrameOrigin->isOpaque() for the non-SubFrame cases — generalizing what previously applied only to InitiatingAction::SubFrame. interceptMarketplaceKitNavigation in NavigationState.mm replaces action->data().requester->topOrigin with SecurityOriginData::fromURL(page.mainFrame()->url()) for both the gating null-check and the requesterTopOriginURL it computes. WebPageProxy::requestGeolocationPermissionForFrame turns the silent if (!frame) return; into MESSAGE_CHECK(process, frame) and overwrites frameInfo.securityOrigin with frame->securityOrigin()->data() whenever frame->url().host() is non-empty.
In the token-scoping cluster: GeolocationPermissionRequestProxy gains a RegistrableDomain m_registrableDomain member and accessor; GeolocationPermissionRequestManagerProxy::m_validAuthorizationTokens changes from HashSet<String> to HashMap<String, RegistrableDomain>, populated in didReceiveGeolocationPermissionDecision; and a registrableDomainForAuthorizationToken() lookup is added. WebGeolocationManagerProxy::startUpdatingWithProxy replaces the boolean isValidAuthorizationToken test with MESSAGE_CHECK(..., !!authorizedDomain) plus MESSAGE_CHECK(..., authorizedDomain->isEmpty() || *authorizedDomain == registrableDomain). Note that the two geolocation dimensions come from different frames: the prompt origin from the requesting frame, the token binding from the main frame's eTLD+1.
Trusting a security-decision key supplied by the very sandboxed component the decision is meant to constrain, instead of re-deriving it from state the privileged side authoritatively owns.
Background
Where this lives. The UI process sits on the far side of the WebContent sandbox boundary. Messages marked DispatchedFrom=WebContent originate inside that sandbox and are, by design, treated as attacker-controlled once WebContent is compromised.
Frame identity on both sides. FrameInfoData is a serializable struct describing a frame — its frameID, request, securityOrigin — that WebContent attaches to many UI-process messages. WebFrameProxy is the UI process's own mirror of the same frame: WebFrameProxy::url() is the URL recorded at commit time and WebFrameProxy::securityOrigin() is the origin the UI process computed for the committed document. Both processes therefore hold a copy of the same fact, and only one of them is authoritative.
Origin vs. registrable domain. SecurityOrigin is the ref-counted origin object with isOpaque()/toString(); SecurityOriginData is its serializable scheme/host/port value type, obtainable via SecurityOriginData::fromURL(url). RegistrableDomain is the eTLD+1 — a coarser site key used for per-site bucketing. A subframe's registrable domain can differ from the page's main-frame registrable domain.
The geolocation grant flow. WebContent sends RequestGeolocationPermissionForFrame; the UI process creates a GeolocationPermissionRequestProxy and asks the embedder's UI delegate. On allow, didReceiveGeolocationPermissionDecision mints a UUID authorization token, stores it, and returns it to WebContent. WebContent later sends StartUpdating carrying that token plus a RegistrableDomain, and startUpdatingWithProxy starts CoreLocation updates bucketed per domain in m_perDomainData.
Platform-service consumers. AppSSO (SOAuthorization) receives an options dictionary including SOAuthorizationOptionInitiatorOrigin, the origin that initiated the authorization flow; InitiatingAction distinguishes Redirect, PopUp, and SubFrame triggers. MarketplaceKit receives alternative-marketplace navigations together with a referring top-origin URL.
MESSAGE_CHECK. A WebKit IPC-validation macro used in UI-process handlers: when the condition fails, the sending WebContent process is terminated rather than the message being processed.
IPC testing API. A test-only facility (IPCTestingAPIEnabled) exposing IPC.addOutgoingMessageListener / IPC.sendMessage to JavaScript, letting a test capture the raw serialized bytes of an outgoing message and replay a modified one — the standard way to simulate a compromised WebContent process in a regression test.
Analysis
This is a confused-deputy / origin-spoofing flaw across the WebContent→UIProcess trust boundary — an authorization-logic bug, not memory corruption.
WebContent (sandboxed) | UI process (privileged)
------------------------ | ------------------------
RequestGeolocationPermission -->| frameInfo.securityOrigin --> embedder prompt
frameInfo.securityOrigin | (pre-fix: used verbatim)
|
DecidePolicyForNavigation ----->| sourceFrame()->securityOrigin() --> AppSSO
originatingFrameInfoData | SOAuthorizationOptionInitiatorOrigin
|
StartUpdating(token, domain) -->| token in HashSet? yes --> CoreLocation
| m_perDomainData.ensure(domain) <- unscoped
|
^ trust boundary the forged value crosses
In each of the four cases the UI process deserializes a value that it also holds an authoritative copy of, and uses the deserialized copy. The SOAuthorization session read the source frame's origin and passed it to AppSSO as the initiator; interceptMarketplaceKitNavigation read requester->topOrigin from the navigation-action payload; requestGeolocationPermissionForFrame handed FrameInfoData::securityOrigin onward to the embedder's requestGeolocationPermissionForFrame: delegate, which is what a browser renders in the prompt. The regression test in SOAuthorizationTests.mm names the untrusted carrier in its comment as originatingFrameInfoData.securityOrigin inside DecidePolicyForNavigationActionAsync; that linkage is relayed from the test comment, since the supplied context does not include the path that populates API::NavigationAction::sourceFrame()'s origin from that message.
The geolocation path had a second, independent gap. startUpdatingWithProxy validated only that the token existed in a HashSet<String>; the RegistrableDomain under which CoreLocation updates were then bucketed came from the WebContent message and was never compared against anything recorded at grant time. A token minted for the loaded page could be spent to start updates under any domain the renderer named.
None of this is reachable from ordinary web content — a normal WebContent process fills these fields truthfully, so the attacker must already be able to emit arbitrary IPC. Given that primitive, the trigger sequences are concrete, and both regression tests demonstrate them by capturing a legitimate outgoing message and byte-replacing localhost with the same-length evil.host before replaying it:
- Prompt spoofing. Send
RequestGeolocationPermissionForFramewith a validframeInfo.frameIDbutframeInfo.securityOriginset to a victim host; the embedder's delegate observes the forged origin. The new API test asserts the delegate now only ever observes the reallocalhost. - Grant redemption outside its scope. Obtain a legitimate token via
didReceiveGeolocationPermissionDecision, then sendStartUpdatingwith that token and an arbitraryregistrableDomain. Pre-fix,m_perDomainData.ensure(registrableDomain, ...)created or reused whatever bucket the renderer named. - AppSSO initiator spoofing. Forge the origin carried in
DecidePolicyForNavigationActionAsync; theSOAuthorizationTests.mmtest asserts the observed initiator origin stayswcptest://localhost. - MarketplaceKit attribution. Forge the requester's
topOriginso the referring top-origin URL names a victim site.
Escalation beyond identity spoofing depends on how each system service treats the value: if AppSSO extensions key credential issuance or flow selection on InitiatorOrigin, a forged value could steer an authorization flow under a victim site's name; if MarketplaceKit gates installs on the referring top origin, a forged value could satisfy that gate. Neither downstream behavior appears in the supplied context, so both remain conditional.
The fix is also deliberately partial. The geolocation override applies only when frame->url().host() / mainFrame->url().host() is non-empty; the in-code comment states that an empty RegistrableDomain means the UI process could not authoritatively determine the domain, and StartUpdating then skips the equality check and accepts the WebContent-supplied domain. Which concrete load types produce an empty frame-URL host is not established by the supplied context, but an attacker who could arrange such a load would still control those fields.
This vulnerability weakens the WebContent→UIProcess trust boundary and, through it, the per-site permission model. The model assumes that the origin a system service is told about reflects the document the UI process actually committed, and that a permission grant is redeemable only within the scope it was issued for. An attacker who already has code execution in WebContent could therefore have platform services and the embedding app apply a different site's identity — a spoofed origin in a prompt the user approves, a location grant redeemed under an unrelated domain bucket, an SSO or app-install flow driven under a victim site's name. This is post-compromise scope escalation: it grants no sandbox escape, but it converts renderer compromise into the cross-site privacy and identity-integrity failure the permission model was supposed to contain.
Insight: the same shape at four independent sites in one process is the signature of an audit sweep, not a bug report. It recurs because the frame-info and navigation-action structs are convenience carriers — WebContent already knows the origin, so serializing it saves the UI process a lookup — while the UI process holds an authoritative copy in WebFrameProxy anyway. Two details are worth carrying forward. The geolocation fix binds two different dimensions from two different frames, so the StartUpdating equality check pins the page's main-frame site, not the origin the user was actually prompted for; a cross-origin subframe grant is still redeemed under the embedder site's bucket. And the SOAuthorizationSession change trades precision for safety on Redirect and PopUp, which previously reported the source frame's origin and now report the main-frame origin — a real semantic downgrade for SSO flows started from a cross-origin subframe, and the kind of change that surfaces later as a functional regression report.
Audit directions
- A privileged receiver consuming a fact from an untrusted sender when it holds an authoritative copy of that same fact. The invariant is that if the receiver can derive it, the transmitted copy is advisory and must never key a security decision. Narrow: grep
Source/WebKit/UIProcessfor handlers takingFrameInfoData&&or a navigation-action payload and reading.securityOrigin/.topOrigin/.urlwithout a nearbyWebFrameProxy::webFrame(frameID)lookup —WebPageProxy.cppmessage handlers andAPI::NavigationActionconsumers are the dense area. Wider: any UI-process consumer of a WebContent-supplied identity field, including media/notification/clipboard permission handlers andWebProcessProxyhandlers taking aSecurityOriginData; look for the field being forwarded to an embedder delegate or soft-linked framework rather than merely logged. Widest: the general client-supplied-authorization-key class — Chromium Mojo handlers that accept an origin instead of usingRenderFrameHost::GetLastCommittedOrigin(), syscalls that trust a userspace credential struct, APIs that trust a client-sentuser_idalongside a session token. Match tell: a value crossing a privilege boundary inbound and then used as a lookup key or identity label on the privileged side. - Capability tokens that authenticate the bearer but do not bind the scope they were issued for — and, where a scope is bound, tokens whose bound dimension is not the dimension the user consented to. Existence checks (
HashSet::contains) authenticate but do not authorize. Narrow: audit the other mint-in-UI, present-from-WebContent flows —WebGeolocationManagerProxyand the analogous request-manager proxies for media capture, notifications, and speech — checking whether the redeem-side handler compares any scope field against what was recorded at grant time, and whether that field matches the granularity the prompt displayed. Wider: any handle minted by a privileged side and later replayed with sibling parameters the privileged side does not re-derive, such as*Identifier-typed handles paired with a separately-supplied page/frame/domain argument. Widest: the classic unscoped-bearer-token class — an OAuth token missing an audience claim, a capability handle without an attached resource identifier. Match tell: a redemption site whose validation is a membership test over an opaque string, or an equality test against a coarser key than the one the user was shown. - Intentional trust exceptions guarded by a sentinel value, where the sentinel means "unverified" and downstream code must honor it. Investigate the empty-
RegistrableDomain/ empty-url().host()carve-out introduced here: enumerate the loader paths that commit a document whose frame URL has an empty host, check whether an attacker can steer a page into that state to re-open the accepted-forgery window, and check whether any other consumer ofm_validAuthorizationTokensor ofFrameInfoData::securityOriginreads the same sentinel with a different meaning. Wider: search UIProcess validators for conditions of the formx.isEmpty() || x == yand ask what makesxempty. Widest: the fail-open compatibility escape hatch — a signature verifier that skips when no key is configured, an ACL that permits when the resource label is unset. In code review, a disjunction whose first branch is an emptiness test and whose second is the actual security comparison should carry a comment justifying the exception. - Converting a silent early-return into a process-terminating validator, which turns benign lifecycle races into availability failures. Verify that the new
MESSAGE_CHECK(process, frame)cannot fire on a legitimate sequence — specifically whether a frame can be detached or navigated in WebContent betweenRequestGeolocationPermissionForFramebeing sent and the UI process runningWebFrameProxy::webFrame(frameInfo.frameID). Wider: grep UIProcess forMESSAGE_CHECKimmediately following a::webFrame(,::webPage(, or similar static lookup, and check whether the identifier's teardown is ordered against the message's send; this generally needs a targeted test racing detach against the message rather than static reading. Match tell: aMESSAGE_CHECKpredicate that is a lookup result rather than a value-range or well-formedness check — lookups can fail from timing, well-formedness checks cannot.