← 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 cross-process trust model behind these four handlers, and what a compromised renderer could actually claim, is examined in depth.

Subscribe to read more

🔒

Four reusable audit patterns identified, with concrete UIProcess starting points for finding sibling instances of this trust-boundary flaw.

Subscribe to read more