← All issues

[3] WebContent-supplied origin forwarded as authorization key at four UI-process IPC sites

Severity: Medium | Component: WebKit UIProcess permission and authorization plumbing | 5ca4d87

Rated Medium because the diff stops four UI-process handlers from keying privileged decisions on an origin/domain a compromised WebContent process can forge, yielding a cross-site permission/identity spoof (e.g. exercising a victim site's geolocation grant); it is not memory corruption and presupposes the attacker can already emit a forged WebContent IPC message, which bounds the severity below the renderer UAFs above.

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 / the geolocation policy decider apply another site's permission decision. The change re-derives each value from UI-process-authoritative state (WebFrameProxy::url() / WebFrameProxy::securityOrigin() / the committed main-frame URL): SOAuthorizationSession derives InitiatorOrigin from mainFrame()->url(); interceptMarketplaceKitNavigation derives the top-origin URL from page.mainFrame()->url(); requestGeolocationPermissionForFrame overwrites FrameInfoData::securityOrigin with the UI-computed origin and MESSAGE_CHECKs the frame; and startUpdatingWithProxy binds the RegistrableDomain to the authorization token and MESSAGE_CHECKs that the WebContent-supplied domain matches. Webarchive/opaque-document loads, whose origin the UIProcess cannot inspect, keep the pre-existing behavior.

Source/WebKit/UIProcess/WebPageProxy.cpp

void WebPageProxy::requestGeolocationPermissionForFrame(IPC::Connection& connection, GeolocationIdentifier geolocationID, FrameInfoData&& frameInfo)
{
+ Ref process = WebProcessProxy::fromConnection(connection);
RefPtr frame = WebFrameProxy::webFrame(frameInfo.frameID);
- if (!frame)
- return;
+ MESSAGE_CHECK(process, frame);
+
+ if (!frame->url().host().isEmpty())
+ frameInfo.securityOrigin = frame->securityOrigin()->data();
+
+ WebCore::RegistrableDomain mainFrameDomain;
+ if (RefPtr mainFrame = m_mainFrame.get(); mainFrame && !mainFrame->url().host().isEmpty())
+ mainFrameDomain = WebCore::RegistrableDomain { mainFrame->url() };
- auto request = protect(internals().geolocationPermissionRequestManager)->createRequest(geolocationID, protect(frame->process()));
+ auto request = protect(internals().geolocationPermissionRequestManager)->createRequest(geolocationID, protect(frame->process()), WTF::move(mainFrameDomain));

Source/WebKit/UIProcess/WebGeolocationManagerProxy.cpp

- auto isValidAuthorizationToken = protect(page->geolocationPermissionRequestManager())->isValidAuthorizationToken(authorizationToken);
- MESSAGE_CHECK(proxy.connection(), isValidAuthorizationToken);
+ auto authorizedDomain = protect(page->geolocationPermissionRequestManager())->registrableDomainForAuthorizationToken(authorizationToken);
+ MESSAGE_CHECK(proxy.connection(), !!authorizedDomain);
+ MESSAGE_CHECK(proxy.connection(), authorizedDomain->isEmpty() || *authorizedDomain == registrableDomain);

Source/WebKit/UIProcess/Cocoa/SOAuthorization/SOAuthorizationSession.mm

- if (RefPtr sourceOrigin = m_navigationAction->sourceFrame() ? m_navigationAction->sourceFrame()->securityOrigin().securityOrigin().ptr() : nullptr; sourceOrigin && !sourceOrigin->isOpaque())
- initiatorOrigin = sourceOrigin->toString();
- if (m_page->mainFrame()) {
- if (m_action == InitiatingAction::SubFrame)
- initiatorOrigin = WebCore::SecurityOrigin::create(m_page->mainFrame()->url())->toString();
+ if (RefPtr mainFrame = page ? page->mainFrame() : nullptr) {
+ Ref mainFrameOrigin = WebCore::SecurityOrigin::create(mainFrame->url());
+ if (m_action == InitiatingAction::SubFrame || !mainFrameOrigin->isOpaque())
+ initiatorOrigin = mainFrameOrigin->toString();

Tools/TestWebKitAPI/Tests/WebKit/WKWebView/PermissionsAPI.mm

+ const realBytes = enc.encode('localhost');
+ const evilBytes = enc.encode('evil.host');
+ // byte-replace real host with forged host in captured IPC and replay
+ EXPECT_WK_STREQ(host, "localhost"_s);
+ EXPECT_FALSE(host.contains("evil"_s));

The geolocation handler adds MESSAGE_CHECK(process, frame) and, when frame->url().host() is non-empty, overwrites frameInfo.securityOrigin with frame->securityOrigin()->data(); it derives a RegistrableDomain from the committed main-frame URL and threads it into createRequest. The geolocation token store changes from HashSet<String> to HashMap<String, RegistrableDomain> so each token carries its bound domain (exposed via registrableDomainForAuthorizationToken); startUpdatingWithProxy then MESSAGE_CHECKs that the WebContent-supplied registrableDomain matches the token's bound domain, skipping the check only when the UI-derived domain is empty. SOAuthorizationSession always derives initiatorOrigin from SecurityOrigin::create(mainFrame->url()), applying the non-opaque check uniformly. interceptMarketplaceKitNavigation computes the top-origin as SecurityOriginData::fromURL(page.mainFrame()->url()).

Trusting a WebContent-supplied origin as an authorization key in the UI process instead of re-deriving it from process-authoritative frame state.

WebKit splits the browser into a sandboxed WebContent process (runs web JS, untrusted) and a UI process (trusted, talks to system services). They communicate over IPC; a message tagged DispatchedFrom=WebContent originates in the untrusted process, so its fields are attacker-influenced once that process is compromised. MESSAGE_CHECK is a WebKit macro that validates an IPC invariant and terminates the sending process on failure. FrameInfoData::securityOrigin is an origin descriptor serialized inside several IPC messages describing a frame. WebFrameProxy is the UI-process mirror of a frame; WebFrameProxy::securityOrigin() and WebFrameProxy::url() return origin/URL values the UI process computed itself at navigation-commit time, independent of anything WebContent sends. RegistrableDomain is the eTLD+1 grouping used as a site key.

A geolocation authorization token is a UUID the UI process mints when the user approves a site; the WebContent process later presents it on StartUpdating to begin receiving positions. AppSSO/SOAuthorization (InitiatorOrigin) and MarketplaceKit (top origin) similarly take an origin from the UI process and hand it to a system service as the identity of the requesting site. The IPC Testing API (IPCTestingAPIEnabled) lets a test capture and replay raw outgoing IPC bytes, modeling a compromised WebContent that emits forged messages.

This is an authorization-key origin-spoofing / confused-deputy logic flaw, not memory corruption. Before the fix, four UI-process handlers used an origin/domain value carried inside a DispatchedFrom=WebContent message as the authoritative per-site key for a privileged decision, without comparing it to state the UI process owns. The WebContent process is the untrusted party; any field it serializes must be treated as attacker-controlled once it is compromised. Concretely, requestGeolocationPermissionForFrame passed FrameInfoData::securityOrigin straight to the embedder's permission UI and later validated StartUpdating only by checking the token existed — never that the requesting domain matched the granted one; SOAuthorizationSession seeded the AppSSO InitiatorOrigin from sourceFrame()->securityOrigin(); and interceptMarketplaceKitNavigation used NavigationRequester::topOrigin.

The geolocation case is the cleanest two-step: the token is minted for the user-approved domain, but StartUpdating never bound the token to that domain, so any forged registrableDomain accompanying a valid token was accepted. The regression tests model the compromised renderer via the IPC Testing API — capturing the real RequestGeolocationPermissionForFrame / DecidePolicyForNavigationActionAsync bytes and byte-replacing the real host (localhost) with a forged one (evil.host) before replay. For geolocation specifically, an attacker obtains (or replays) a valid token and sends StartUpdating with a forged registrableDomain; pre-fix, isValidAuthorizationToken accepted the token regardless of domain, so positions could be obtained under a spoofed site key. For AppSSO/MarketplaceKit, forging the source-frame/top origin makes the system service treat the request as initiated by the victim origin.

This vulnerability weakens cross-process origin isolation. The UI process is the trusted deputy that converts a site's identity into a privileged grant, and before the fix that identity could be supplied by the untrusted WebContent process. The affected invariant is that a per-site permission decision (geolocation access, AppSSO initiator origin, MarketplaceKit top origin) is keyed to the origin actually committed in the frame, as known UI-side. An attacker who has already compromised a WebContent process could have a system service apply another site's permission decision — exercising a victim site's standing geolocation grant, or presenting a victim origin to AppSSO/MarketplaceKit — a cross-site permission/identity spoof that presupposes the ability to send a forged WebContent IPC message.

This is one bug class found at four independent sites, which strongly suggests the same anti-pattern is latent at other UI-process handlers consuming FrameInfoData::securityOrigin, NavigationRequester::topOrigin, or originatingFrameInfoData.securityOrigin. The webarchive/opaque-document carve-out (empty UI-derived domain ⇒ skip the equality check) is itself a residual soft spot worth tracking — it preserves the old trust-the-WebContent behavior for loads whose origin the UI process cannot authoritatively inspect.

Note: The SOAuthorizationSession enclosing function name, the precise pre-fix geolocation delegate-dispatch path, and the NavigationRequester type name are inferred from the commit message and code shape rather than fully shown in the diff. The four trust-boundary gaps and the uniform re-derivation fix are directly supported by the patch.