← 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

Medium으로 평가된 이유는, 이 diff가 네 곳의 UI process handler에서 손상된 WebContent process가 위조할 수 있는 origin/domain 값을 권한 결정의 key로 사용하는 패턴을 차단하기 때문입니다. cross-site permission/identity spoof(예: 피해자 사이트의 geolocation 권한 행사)를 막는 변경이지만, memory corruption은 아닙니다. 또한 공격자가 이미 위조된 WebContent IPC 메시지를 전송할 수 있는 상태를 전제로 하기 때문에, severity는 앞서 다룬 renderer UAF보다 낮게 설정되었습니다.

네 곳의 UI process IPC handler는 DispatchedFrom=WebContent 메시지에 포함된 origin/domain 값을 그대로 시스템 서비스에 per-site authorization key로 전달하고 있었습니다. UI process가 직접 보유한 상태와 비교하는 과정이 없었습니다. 손상된 WebContent process가 origin을 위조하면, CoreLocation / AppSSO / MarketplaceKit / geolocation policy decider가 다른 사이트의 권한 결정을 적용하는 상황이 발생할 수 있었습니다.

이번 변경에서는 각 값을 UI process가 직접 보유한 상태에서 다시 도출하도록 수정되었습니다. 활용되는 기준값은 WebFrameProxy::url(), WebFrameProxy::securityOrigin(), commit된 main-frame URL입니다. SOAuthorizationSessionInitiatorOriginmainFrame()->url()에서 도출하고, interceptMarketplaceKitNavigation은 top-origin URL을 page.mainFrame()->url()에서 도출합니다. requestGeolocationPermissionForFrameFrameInfoData::securityOrigin을 UI가 계산한 origin으로 덮어쓰고 frame에 대해 MESSAGE_CHECK를 수행합니다. startUpdatingWithProxy는 authorization token에 RegistrableDomain을 바인딩하고, WebContent가 전달한 domain이 이와 일치하는지 MESSAGE_CHECK로 검증합니다. UI process가 origin을 직접 확인할 수 없는 webarchive/opaque-document 로드에는 기존 동작이 유지됩니다.

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));

geolocation handler에 MESSAGE_CHECK(process, frame)가 추가되었습니다. frame->url().host()가 비어 있지 않은 경우, frameInfo.securityOriginframe->securityOrigin()->data()로 덮어씁니다. commit된 main-frame URL에서 RegistrableDomain을 도출하여 createRequest에 전달합니다.

geolocation token 저장소는 HashSet<String>에서 HashMap<String, RegistrableDomain>으로 변경되었습니다. 이로써 각 token이 바인딩된 domain을 함께 보유하게 되었으며, 해당 값은 registrableDomainForAuthorizationToken을 통해 조회할 수 있습니다. startUpdatingWithProxy에서는 WebContent가 전달한 registrableDomain이 token에 바인딩된 domain과 일치하는지 MESSAGE_CHECK로 검증합니다. 단, UI에서 도출한 domain이 비어 있는 경우에는 이 검증을 건너뜁니다.

SOAuthorizationSessioninitiatorOrigin을 항상 SecurityOrigin::create(mainFrame->url())에서 도출하며, non-opaque 여부 확인을 일관되게 적용합니다. interceptMarketplaceKitNavigation은 top-origin을 SecurityOriginData::fromURL(page.mainFrame()->url())로 계산합니다.

UI process에서 WebContent가 전달한 origin을 그대로 authorization key로 신뢰하고, process가 직접 보유한 frame 상태로부터 재도출하지 않은 패턴.

WebKit은 브라우저를 샌드박스 처리된 WebContent process(웹 JavaScript를 실행하는 비신뢰 영역)와 UI process(신뢰 영역, 시스템 서비스와 통신)로 분리합니다. 양쪽은 IPC를 통해 통신하며, DispatchedFrom=WebContent로 태깅된 메시지는 비신뢰 프로세스에서 발원합니다. 해당 프로세스가 손상되면 메시지의 모든 필드는 공격자가 제어할 수 있는 값으로 간주해야 합니다. MESSAGE_CHECK는 IPC invariant를 검증하는 WebKit 매크로로, 검증 실패 시 송신 프로세스를 종료합니다. FrameInfoData::securityOrigin은 frame을 설명하는 여러 IPC 메시지 안에 직렬화되는 origin 기술자입니다. WebFrameProxy는 frame의 UI process 측 미러로, WebFrameProxy::securityOrigin()WebFrameProxy::url()은 WebContent가 전달하는 값과 무관하게 UI process가 navigation commit 시점에 직접 계산한 origin/URL을 반환합니다. RegistrableDomain은 site key로 사용되는 eTLD+1 단위 그룹입니다.

geolocation authorization token은 사용자가 특정 사이트를 승인할 때 UI process가 생성하는 UUID입니다. WebContent process는 이후 StartUpdating 시점에 이 token을 제출해 위치 정보 수신을 시작합니다. AppSSO/SOAuthorization(InitiatorOrigin)과 MarketplaceKit(top origin)도 마찬가지로 UI process에서 origin을 받아 요청 사이트의 신원으로서 시스템 서비스에 전달합니다. IPC Testing API(IPCTestingAPIEnabled)는 외부로 나가는 IPC 바이트를 캡처하고 재전송할 수 있는 테스트 도구로, 위조 메시지를 전송하는 손상된 WebContent를 모델링하는 데 활용됩니다.

이 취약점은 memory corruption이 아니라 authorization key origin spoofing 및 confused-deputy 로직 결함에 해당합니다. 패치 이전에는 네 곳의 UI process handler가 DispatchedFrom=WebContent 메시지에 포함된 origin/domain 값을 UI process 자체 상태와 비교하지 않고, per-site 권한 결정의 authoritative key로 그대로 사용하고 있었습니다. WebContent process는 비신뢰 대상입니다. 해당 프로세스가 손상되면, 직렬화하는 모든 필드는 공격자가 제어할 수 있는 값으로 간주해야 합니다.

구체적으로, requestGeolocationPermissionForFrameFrameInfoData::securityOrigin을 embedder의 권한 UI에 그대로 전달했습니다. StartUpdating 검증에서는 token의 존재 여부만 확인했을 뿐, 요청 domain이 권한을 부여받은 domain과 일치하는지는 검증하지 않았습니다. SOAuthorizationSession은 AppSSO InitiatorOriginsourceFrame()->securityOrigin()에서 도출했고, interceptMarketplaceKitNavigationNavigationRequester::topOrigin을 사용했습니다.

🔒

The cross-process trust model behind these four handlers, and what a compromised renderer could actually claim, is examined in depth.

더 확인하려면 구독해 주세요

🔒

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

더 확인하려면 구독해 주세요