← 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을 사용했습니다.

geolocation 사례는 두 단계로 이루어진 가장 명확한 케이스입니다. token은 사용자가 승인한 domain을 기준으로 발급되었지만, StartUpdating에서 해당 domain에 token을 바인딩하는 처리가 없었습니다. 유효한 token에 위조된 registrableDomain을 함께 전송하면 그대로 수락되는 상태였습니다.

regression test는 IPC Testing API를 통해 손상된 renderer를 모델링합니다. 실제 RequestGeolocationPermissionForFrame / DecidePolicyForNavigationActionAsync 바이트를 캡처한 뒤, 실제 host(localhost)를 위조된 host(evil.host)로 바이트 단위로 교체해 재전송하는 방식입니다.

geolocation 시나리오에서 공격자는 유효한 token을 확보하거나 재전송한 뒤, 위조된 registrableDomain을 담아 StartUpdating을 전송합니다. 패치 이전에는 isValidAuthorizationToken이 domain과 무관하게 token의 존재만 확인했기 때문에, 위조된 site key로 위치 정보를 수신하는 것이 가능했습니다. AppSSO/MarketplaceKit 시나리오에서는 source-frame/top origin을 위조하면 시스템 서비스가 해당 요청을 피해자 origin에서 발신된 것으로 처리합니다.

이 취약점은 cross-process origin isolation을 약화시킵니다. UI process는 사이트의 신원을 권한 부여로 변환하는 신뢰된 대리자(trusted deputy) 역할을 합니다. 그런데 패치 이전에는 그 신원을 비신뢰 WebContent process가 직접 제공할 수 있었습니다.

이번에 확인된 invariant는 다음과 같습니다. per-site 권한 결정은 UI 측에서 인지하는 frame에 실제로 commit된 origin을 기준으로 결정되어야 합니다. 대상은 geolocation 접근, AppSSO initiator origin, MarketplaceKit top origin입니다. WebContent process를 이미 손상시킨 공격자는 시스템 서비스가 다른 사이트의 권한 결정을 적용하게 만들 수 있었습니다. 피해자 사이트에 부여된 geolocation 권한을 행사하거나, AppSSO/MarketplaceKit에 피해자 origin을 제시하는 방식입니다. 이는 위조된 WebContent IPC 메시지를 전송할 수 있는 능력을 전제로 하는 cross-site permission/identity spoof에 해당합니다.

동일한 bug class가 네 곳의 독립적인 지점에서 발견되었습니다. 이는 FrameInfoData::securityOrigin, NavigationRequester::topOrigin, originatingFrameInfoData.securityOrigin을 사용하는 다른 UI process handler에도 같은 anti-pattern이 잠재되어 있을 가능성을 강하게 시사합니다.

webarchive/opaque-document 예외 처리(UI에서 도출한 domain이 비어 있는 경우 → 동등성 검사 생략)는 그 자체로 추적이 필요한 잔여 취약 지점입니다. UI process가 직접 확인할 수 없는 로드에 대해 WebContent를 신뢰하던 기존 동작이 여전히 유지되고 있기 때문입니다.

Note: SOAuthorizationSession의 enclosing 함수 이름, 패치 이전 geolocation delegate-dispatch의 정확한 경로, NavigationRequester 타입 이름은 diff에 직접 드러나지 않으며, commit 메시지와 코드 구조로부터 추론된 사항입니다. 네 곳의 trust boundary 결함과 일관된 재도출 방식으로의 수정은 patch에서 직접 확인됩니다.