[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. Privileged process 안에 있는 네 개의 독립적인 handler가 sandbox 내부에서 전달받은 site identity를 permission 또는 platform-service 판단의 key로 사용했습니다. Identity spoofing이 실제 escalation으로 이어지는지는 각 system service가 해당 값을 어떻게 처리하는지에 달려 있습니다. 다만 geolocation 경로 하나만 보더라도, 사용자가 승인 여부를 판단해야 하는 프롬프트에 엉뚱한 사이트 이름이 표시되는 상황이 발생합니다.
WebKit은 브라우징 세션을 두 개의 process로 나눠 처리합니다. Site content를 파싱하고 실행하는 sandbox된 WebContent process가 있고, navigation state를 소유하며 permission prompt를 렌더링하고 AppSSO, MarketplaceKit, CoreLocation 같은 platform service의 client 역할을 수행하는 UI process가 있습니다. UI process는 각 frame에 대해 자체적인 사본을 유지합니다. 이것이 WebFrameProxy이며, document가 commit되는 시점에 url()과 securityOrigin()이 기록됩니다. 한편 WebContent는 자신이 보내는 여러 message에 직렬화 가능한 FrameInfoData frame 설명을 함께 첨부합니다. 원래 기대되는 동작은, UI process가 다른 곳으로 넘기는 site identity가 실제로 commit된 document를 정확히 반영하는 것입니다.
관전 포인트: 침해된 WebContent process가 geolocation prompt, AppSSO authorization, MarketplaceKit install에 피해 사이트의 이름을 올릴 수 있고, 자신이 지정하는 임의의 registrable domain으로 location grant를 redeem할 수 있습니다.
UI process의 IPC handler 네 곳은 DispatchedFrom=WebContent message에서 origin/domain 필드를 받아, 이를 UI-process-authoritative state와 비교하는 절차 없이 system service에 그대로 전달해 사이트별 authorization key로 사용했습니다. 침해된 WebContent process가 origin을 spoof하면, CoreLocation·AppSSO·MarketplaceKit이나 geolocation policy decider가 다른 사이트에 대한 permission 결정을 적용하게 만들 수 있습니다. 이번 변경으로 IPC로 전달된 필드를 그대로 신뢰하는 대신, 각 값을 UI-process-authoritative state로부터 다시 유도하도록 수정되었습니다.
Source/WebKit/UIProcess/WebPageProxy.cpp
Source/WebKit/UIProcess/WebGeolocationManagerProxy.cpp
Source/WebKit/UIProcess/Cocoa/SOAuthorization/SOAuthorizationSession.mm
| 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 | |
| ``` |
네 가지 case 모두에서, UI process는 자신이 authoritative한 사본을 이미 보유하고 있는 값을 역직렬화해서 그 역직렬화된 사본을 사용합니다. SOAuthorization session은 source frame의 origin을 읽어 AppSSO에 initiator로 전달했습니다. interceptMarketplaceKitNavigation은 navigation-action payload에서 requester->topOrigin을 읽었습니다. requestGeolocationPermissionForFrame은 FrameInfoData::securityOrigin을 embedder의 requestGeolocationPermissionForFrame: delegate로 그대로 전달했는데, 이는 브라우저가 prompt에 렌더링하는 값입니다. SOAuthorizationTests.mm의 regression test는 comment에서 신뢰할 수 없는 carrier를 DecidePolicyForNavigationActionAsync 내부의 originatingFrameInfoData.securityOrigin으로 명시하고 있습니다. 이 연결 관계는 test comment에서 그대로 인용한 것으로, 해당 메시지로부터 API::NavigationAction::sourceFrame()의 origin을 채우는 경로 자체는 제공된 context에 포함되어 있지 않습니다.
Geolocation 경로에는 별개의 gap이 하나 더 있었습니다. startUpdatingWithProxy는 token이 HashSet<String> 안에 존재하는지만 검증했을 뿐, CoreLocation update가 bucketing되는 기준인 RegistrableDomain은 WebContent 메시지에서 그대로 가져와서 grant 시점에 기록된 어떤 값과도 비교하지 않았습니다. 즉 로드된 페이지에 대해 발급된 token을, renderer가 지정한 임의의 domain으로 update를 시작하는 데 사용할 수 있었습니다.
이 문제들은 일반적인 web content에서는 도달할 수 없습니다. 정상적인 WebContent process는 이런 필드를 진실하게 채우기 때문에, attacker는 이미 임의의 IPC를 emit할 수 있는 능력을 갖추고 있어야 합니다. 다만 그 primitive가 주어졌을 때 trigger sequence는 구체적이며, 두 regression test 모두 정상적인 outgoing message를 캡처해 localhost를 같은 길이의 evil.host로 byte-replace한 뒤 replay하는 방식으로 이를 증명합니다.
- Prompt spoofing. 유효한
frameInfo.frameID와 함께frameInfo.securityOrigin을 victim host로 설정해RequestGeolocationPermissionForFrame을 전송합니다. Embedder의 delegate는 위조된 origin을 관찰하게 됩니다. 새로 추가된 API test는 이제 delegate가 오직 실제localhost만 관찰한다는 것을 검증합니다. - Grant redemption이 scope 밖에서 이루어지는 경우.
didReceiveGeolocationPermissionDecision을 통해 정상적인 token을 획득한 뒤, 그 token과 임의의registrableDomain으로StartUpdating을 전송합니다. 패치 이전에는m_perDomainData.ensure(registrableDomain, ...)가 renderer가 지정한 임의의 bucket을 생성하거나 재사용했습니다. - AppSSO initiator spoofing.
DecidePolicyForNavigationActionAsync가 전달하는 origin을 위조합니다.SOAuthorizationTests.mmtest는 관찰된 initiator origin이wcptest://localhost로 유지되는지를 검증합니다. - MarketplaceKit attribution. Requester의
topOrigin을 위조해, referring top-origin URL이 victim site를 가리키도록 만듭니다.
Identity spoofing을 넘어서는 확장 가능성은 각 system service가 해당 값을 어떻게 처리하는지에 달려 있습니다. AppSSO extension이 credential 발급이나 flow 선택을 InitiatorOrigin에 근거해 결정한다면, 위조된 값이 victim site 명의로 authorization flow를 조작하는 데로 이어질 가능성이 있습니다. 마찬가지로 MarketplaceKit이 referring top origin을 기준으로 install을 gating한다면, 위조된 값이 그 gate를 통과시킬 가능성도 있습니다. 다만 이러한 downstream 동작은 제공된 context에 나타나지 않으므로, 두 시나리오 모두 조건부로 남아 있습니다.
이번 fix 역시 의도적으로 부분적입니다. Geolocation override는 frame->url().host() / mainFrame->url().host()가 비어있지 않은 경우에만 적용됩니다. 코드 내 comment에 따르면, RegistrableDomain이 비어있다는 것은 UI process가 domain을 authoritative하게 판별할 수 없었다는 의미이며, 이 경우 StartUpdating은 equality check를 건너뛰고 WebContent가 제공한 domain을 그대로 받아들입니다. 어떤 구체적인 로드 유형이 비어있는 frame-URL host를 만들어내는지는 제공된 context만으로는 확정되지 않지만, 그러한 로드를 유도할 수 있는 attacker라면 여전히 이 필드들을 제어할 수 있게 됩니다.
이 취약점은 WebContent→UIProcess trust boundary를 약화시키고, 이를 통해 site별 permission model 전체를 약화시킵니다. 이 model은 system service에 전달되는 origin이 UI process가 실제로 commit한 document를 반영한다는 것, 그리고 permission grant가 발급된 scope 내에서만 redeem 가능하다는 것을 전제로 합니다. 이미 WebContent에서 code execution을 확보한 attacker라면, platform service와 embedding app이 다른 site의 identity로 동작하게 만들 수 있습니다. 사용자가 승인하는 prompt에 위조된 origin이 표시되거나, location grant가 관련 없는 domain bucket 하에서 redeem되거나, SSO나 app-install flow가 victim site 명의로 진행될 수 있습니다. 이는 post-compromise scope escalation에 해당합니다. Sandbox escape를 제공하지는 않지만, renderer compromise를 permission model이 막고자 했던 cross-site privacy 및 identity-integrity 실패로 전환시킵니다.
Insight: 같은 형태의 문제가 한 process 안에서 서로 독립된 네 곳에 나타난다는 것은 개별 bug report가 아니라 audit sweep의 흔적입니다. 이런 패턴이 반복되는 이유는 frame-info와 navigation-action struct가 본질적으로 편의용 carrier이기 때문입니다. WebContent는 이미 origin을 알고 있으므로 이를 직렬화해 전달하면 UI process의 lookup을 절약할 수 있지만, UI process는 어차피 WebFrameProxy에 authoritative한 사본을 보유하고 있습니다. 여기서 두 가지 세부 사항은 눈여겨볼 만합니다. Geolocation fix는 서로 다른 두 frame에서 나온 두 dimension을 binding합니다. 즉 StartUpdating equality check는 사용자가 실제로 prompt받은 origin이 아니라 페이지의 main-frame site를 고정합니다. 따라서 cross-origin subframe에 대한 grant는 여전히 embedder site의 bucket 하에서 redeem됩니다. 또한 SOAuthorizationSession 변경은 Redirect와 PopUp에 대해 정밀도를 안전성과 맞바꿉니다. 기존에는 source frame의 origin을 보고했지만 이제는 main-frame origin을 보고하는데, 이는 cross-origin subframe에서 시작된 SSO flow에 실질적인 semantic downgrade이며, 이후 기능적 regression report로 다시 표면화될 수 있는 성격의 변경입니다.
Audit directions
-
Privileged한 receiver가, 자신이 이미 authoritative한 사본을 보유하고 있는 사실을 신뢰할 수 없는 sender로부터 받은 값으로 대체해 사용하는 패턴. 핵심 invariant는, receiver가 해당 값을 스스로 derive할 수 있다면 전달받은 사본은 어디까지나 참고용이며 결코 보안 결정의 key가 되어서는 안 된다는 것입니다. 좁게는,
Source/WebKit/UIProcess에서FrameInfoData&&나 navigation-action payload를 받는 handler 중 근처에WebFrameProxy::webFrame(frameID)lookup 없이.securityOrigin/.topOrigin/.url을 읽는 곳을 grep해보아야 합니다.WebPageProxy.cpp의 message handler와API::NavigationActionconsumer가 밀집된 영역입니다. 넓게는, media/notification/clipboard permission handler를 포함해 WebContent가 제공한 identity 필드를 소비하는 모든 UI-process consumer, 그리고SecurityOriginData를 받는WebProcessProxyhandler를 살펴볼 필요가 있습니다. 이때 해당 필드가 단순히 로깅되는 데 그치지 않고 embedder delegate나 soft-linked framework로 전달되는지를 확인해야 합니다. 가장 넓게는, client-supplied-authorization-key 부류 전반이 대상입니다.RenderFrameHost::GetLastCommittedOrigin()을 쓰는 대신 origin을 그대로 받아들이는 Chromium Mojo handler, userspace credential struct를 신뢰하는 syscall, session token과 함께 전송된 client-supplieduser_id를 신뢰하는 API 등이 여기에 해당합니다. 판별 기준은, privilege boundary를 inbound로 넘어온 값이 privileged 쪽에서 lookup key나 identity label로 사용되는지 여부입니다. -
Bearer는 인증하지만 발급받은 scope는 binding하지 않는 capability token — scope가 binding된 경우라도, 그 binding된 dimension이 사용자가 동의한 dimension과 다른 경우까지 포함합니다.
HashSet::contains같은 존재 여부 검사는 인증(authenticate)은 하지만 인가(authorize)는 하지 않습니다. 좁게는, 다른 mint-in-UI, present-from-WebContent 흐름들을 감사할 필요가 있습니다.WebGeolocationManagerProxy와 media capture, notification, speech에 대한 유사한 request-manager proxy들을 대상으로, redeem-side handler가 grant 시점에 기록된 어떤 scope 필드와도 비교를 수행하는지, 그리고 그 필드가 prompt에 표시된 granularity와 일치하는지를 확인해야 합니다. 넓게는, privileged 측에서 발급한 뒤 privileged 측이 재계산하지 않는 sibling parameter와 함께 나중에 replay되는 모든 handle을 살펴볼 필요가 있습니다. 예를 들어*Identifier타입의 handle이 별도로 전달된 page/frame/domain 인자와 함께 쓰이는 경우가 이에 해당합니다. 가장 넓게는, audience claim이 없는 OAuth token이나 attached resource identifier가 없는 capability handle 같은 고전적인 unscoped-bearer-token 부류가 대상입니다. 판별 기준은, redemption site의 검증이 opaque string에 대한 membership test이거나, 사용자에게 보여준 것보다 더 거친 key와의 equality test인지 여부입니다. -
Sentinel 값으로 지정된 의도적 trust exception이며, sentinel이 "unverified"를 의미할 때 downstream 코드가 이를 반드시 존중해야 하는 경우. 여기서 도입된 empty-
RegistrableDomain/ empty-url().host()carve-out을 조사할 필요가 있습니다. 먼저 frame URL의 host가 비어 있는 document를 커밋하는 loader 경로를 모두 나열해야 합니다. 이어서 attacker가 페이지를 그런 상태로 유도해 accepted-forgery window를 다시 열 수 있는지 확인해야 합니다. 그리고m_validAuthorizationTokens나FrameInfoData::securityOrigin의 다른 consumer가 같은 sentinel을 다른 의미로 읽고 있지는 않은지도 점검해야 합니다. 범위를 넓히면, UIProcess의 validator 중x.isEmpty() || x == y형태의 조건을 검색해 무엇이x를 비게 만드는지 물어야 합니다. 가장 넓게 보면, 이는 fail-open 방식의 compatibility escape hatch에 해당합니다. key가 설정되지 않았을 때 검증을 건너뛰는 signature verifier, resource label이 설정되지 않았을 때 허용하는 ACL이 같은 패턴입니다. 코드 리뷰 시, 첫 번째 branch가 emptiness test이고 두 번째 branch가 실제 security comparison인 disjunction에는 그 예외를 정당화하는 comment가 반드시 있어야 합니다. -
silent early-return을 process를 종료시키는 validator로 바꾸는 패턴이며, 이로 인해 무해한 lifecycle race가 availability failure로 이어질 수 있습니다. 새로 추가된
MESSAGE_CHECK(process, frame)이 정상적인 sequence에서는 발동하지 않는지 확인해야 합니다. 구체적으로는,RequestGeolocationPermissionForFrame이 전송된 시점과 UI process가WebFrameProxy::webFrame(frameInfo.frameID)를 실행하는 시점 사이에 WebContent에서 frame이 detach되거나 navigate될 수 있는지가 관건입니다. 범위를 넓히면, UIProcess 전체에서::webFrame(,::webPage(와 같은 static lookup 직후에 오는MESSAGE_CHECK를 검색하고, 해당 identifier의 teardown이 메시지 전송과 어떤 순서로 일어나는지 확인해야 합니다. 이 부분은 대체로 정적 코드 읽기만으로는 확인되지 않으며, detach와 메시지를 경쟁시키는 targeted test가 필요합니다. 판별 기준은 다음과 같습니다.MESSAGE_CHECK의 predicate가 value-range나 well-formedness check가 아니라 lookup 결과라면 주의해야 합니다. Lookup은 timing에 따라 실패할 수 있지만, well-formedness check는 그렇지 않기 때문입니다.