[1] NetworkProcess parked loader claimable by any WebContent process
A compromised renderer could take another navigation's response just by naming it.
Medium. 피해 범위는 memory corruption이 아니라 renderer 간 기밀성 침해에 해당합니다. 실제로 악용하려면 같은 network session을 공유하는 WebContent process가 먼저 장악되어 있어야 합니다. 노출될 수 있는 최대 범위는 parking 구간 안에 있는 navigation 하나의 main-resource response입니다.
진행 중이던 network load를 한 renderer에서 다음 renderer로 넘기는 privileged broker라면, 그 사이에 다른 renderer가 이를 가로채지 못하도록 보장해야 합니다. WebKit의 NetworkProcess는 navigation response 시점에 process swap이 발생하면, 진행 중인 NetworkResourceLoader를 session 단위 map에 보관합니다. NetworkResourceLoader는 load마다 생성되는 객체로, request parameter와 network task, response 전달을 담당합니다. 이 loader는 UIProcess가 목적지 process를 결정할 때까지 대기 상태로 남게 됩니다. 이때 지켜져야 하는 invariant는 선택된 process만 이 loader를 재개할 수 있다는 점입니다. 재개 요청은 parked loader의 identifier를 담은 ScheduleResourceLoad로 전달되는데, 이 message는 load를 시작하거나 재개하는 IPC message입니다.
관전 포인트: 다른 navigation과 network session을 공유하는 renderer가 장악된 상황을 가정할 수 있습니다. 이 renderer는 순차적으로 증가하는 identifier만 지정하면 해당 navigation의 진행 중인 main-resource load를 가로챌 수 있습니다. 결과적으로 UI process가 다른 process로 보내려던 response를 대신 수신하게 되며, 그 process는 cross-site일 수도 있습니다.
NetworkConnectionToWebProcess::scheduleResourceLoad의existingLoaderToResume에 ownership check가 추가되었습니다. BrowsingContextGroup 전환이 필요하면 NetworkProcess는 새 WebContent process를 띄우는 동안, 진행 중인NetworkResourceLoader를NetworkSession::m_loadersAwaitingWebProcessTransfer에 보관합니다. 이 map의 key는 정수 기반ObjectIdentifier입니다. 문제는 기존scheduleResourceLoad가 WebContent가 전달한 ID를 그대로 받아들였다는 점입니다. 호출자가 해당 parked loader의 소유자인지는 확인하지 않았습니다. 패치 이후에는 UIProcess의TriggerBrowsingContextGroupSwitchForNavigationcompletion handler가 목적지WebProcessIdentifier를 반환합니다. 이 값은 cache된 loader에 기록되고, 호출한 connection과 비교됩니다. 목적지가 정해지기 전에 도착한 claim은 queue에 보관되며(최대 4개), 목적지가 확정되는 시점에 처리됩니다. 소유자가 일치하지 않는 claimant는 종료됩니다. map key를 추측하기 어렵게 만드는 후속 작업도 예정되어 있습니다. 한편 layout test도 함께 추가되었습니다. 이 test는ENABLE(IPC_TESTING_API)로 gate된 test 전용 IPC message 두 개를 이용해, 실제ScheduleResourceLoad경로에서 check가 동작하는지 검증합니다.
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
Source/WebKit/NetworkProcess/NetworkSession.cpp
Patch Details
이번 변경으로 UIProcess가 선택한 destination이 NetworkProcess까지 다시 전달되도록 수정되었습니다. NetworkProcess는 이 값을 기준으로 takeLoaderAwaitingWebProcessTransfer의 claim을 검증합니다. 또한 destination이 확정되기 전에 도착한 claim은 queue에 보관되도록 변경되었습니다.
Destination 전달 경로. 기존에는 ConsiderProcessSwapForNavigationResponse의 completion handler가 bool success만 반환했습니다. 이제는 std::optional<WebCore::ProcessIdentifier> destinationWebProcess를 반환합니다. 이 변경은 NetworkProcessProxy::considerProcessSwapForNavigationResponse, WebPageProxy::triggerBrowsingContextGroupSwitchForNavigation, triggerProcessSwapForEnhancedSecurity, performProcessSwapForNavigationResponse 전체에 반영되었습니다. 이 중 마지막 함수는 continueNavigationInNewProcess 호출 전에 processForNavigation->coreProcessIdentifier()를 미리 저장합니다. 한편 NetworkResourceLoader::sendDidReceiveResponseWithPotentialProcessSwap에서는 swap 결과를 처리하는 lambda가 destination을 받으면 NetworkSession::setParkedLoaderDestinationAndResolvePendingClaims를 호출합니다. 이 함수는 해당 값을 CachedNetworkResourceLoader wrapper에 기록합니다. Enhanced-Security 경로에서 swap이 거절되는 경우에는 새로 추가된 takeParkedLoaderForOriginalProcess가 사용됩니다. 이 함수는 명시적으로 검증을 생략하며, loader를 원래 park했던 connection으로 반환합니다.
검증이 추가된 claim. takeLoaderAwaitingWebProcessTransfer는 이제 호출자의 ProcessIdentifier를 인자로 받습니다. 반환값은 LoaderAwaitingWebProcessTransferClaim이며, Success, NotFound, Pending, WrongCaller 네 가지 결과 중 하나를 담습니다. scheduleResourceLoad는 이 결과에 따라 분기합니다. Success이면 새로 추가된 completeQueuedExistingLoaderResume을 통해 loader를 재개하고, WrongCaller이면 MESSAGE_CHECK(false)에 걸립니다. Pending이면 queuePendingLoaderClaim을 호출하는데, queue가 가득 찬 경우에도 MESSAGE_CHECK에 걸립니다. 마지막으로 NotFound이면 그대로 새 load로 진행합니다.
대기 중인 claim. CachedNetworkResourceLoader에는 세 가지 멤버가 추가되었습니다. 먼저 m_destinationWebProcess가 있습니다. 다음으로 m_pendingClaims가 있는데, 각 항목은 WeakPtr<NetworkConnectionToWebProcess>와 NetworkResourceLoadParameters를 담은 PendingClaim입니다. 그리고 maxPendingClaims = 4가 추가되었습니다. Destination이 도착하면 queue가 순서대로 처리됩니다. 이때 처음으로 일치하는 claim이 loader를 가져갑니다. 다른 process에서 온 claim은 모두 terminateForInvalidLoaderResumeClaim을 호출하며, 이 함수는 NetworkProcessProxy::TerminateWebProcess를 전송합니다. 반면 올바른 process에서 뒤늦게 들어온 중복 claim은 무시됩니다.
테스트 관련 변경. AddSyntheticParkedLoaderForTesting, RemoveSyntheticParkedLoaderForTesting message와 createForTesting constructor가 추가되었습니다. IPCTestingAPI에는 webPageProxyIdentifierForMessageArguments getter가 추가되었습니다. 또한 WrongCaller claim에서 MESSAGE_CHECK가 발생하는지 확인하는 layout test도 포함되었습니다.
추측 가능한 객체 identifier를 보유했다는 사실만으로 privileged broker가 소유권을 인정하는 패턴.
Background
이 코드가 있는 위치. WebKit은 WebContent, GPU, Networking, UI process를 각각 분리해 실행합니다. 이 중 신뢰할 수 없는 쪽은 WebContent입니다. 그래서 NetworkProcess의 IPC handler는 renderer가 이미 compromise되었다고 가정하고, sender가 제공한 모든 인자를 검증해야 합니다.
Navigation response 시점의 process swap.
Navigation response를 다른 WebContent process에서 처리해야 하는 경우가 있습니다. BrowsingContextGroup이 바뀌는 경우나, insecure HTTP response에 대한 Enhanced-Security heuristic이 적용되는 경우가 여기에 해당합니다. 이때 NetworkProcess는 load를 중단하지 않습니다. 대신 loader를 park해 두고, UIProcess에 ConsiderProcessSwapForNavigationResponse로 process 선택을 요청합니다. 그러면 UIProcess는 existingNetworkResourceLoadIdentifierToResume과 함께 continueNavigationInNewProcess를 호출합니다. 이어서 새 WebContent process가 existingLoaderToResume을 담은 ScheduleResourceLoad를 전송합니다. 결과적으로 리소스를 다시 fetch하지 않고 park된 loader를 넘겨받게 됩니다.
Park된 loader를 담는 map.
NetworkSession::m_loadersAwaitingWebProcessTransfer는 NetworkResourceLoadIdentifier를 CachedNetworkResourceLoader에 매핑합니다. CachedNetworkResourceLoader는 RefPtr<NetworkResourceLoader>와 만료 timer를 함께 보관하는 wrapper입니다.
Identity와 검증.
WebCore::ProcessIdentifier는 UIProcess가 WebContent process에 부여하는 식별자입니다. NetworkConnectionToWebProcess는 생성 시점에 이 값을 m_webProcessIdentifier로 저장합니다. 한편 MESSAGE_CHECK는 WebKit의 IPC 검증 macro입니다. Assertion이 실패하면 해당 message는 invalid로 처리되고, 이를 보낸 process는 종료됩니다.
Analysis
Root cause는 IPC handler에서 authorization check가 누락된 것으로, 전형적인 insecure direct object reference에 해당합니다. 패치 이전의 m_loadersAwaitingWebProcessTransfer는 park된 loader의 소유자를 전혀 기록하지 않았습니다. scheduleResourceLoad는 WebContent가 보낸 인자에서 existingLoaderToResume을 그대로 꺼내 takeLoaderAwaitingWebProcessTransfer(*existingLoaderToResume)를 호출했습니다. 이어서 그 결과를 자신의 m_networkResourceLoaders에 추가하고 transferToNewWebProcess(*this, loadParameters)를 실행했습니다. 이 함수는 진행 중인 loader를 호출한 connection에 다시 연결합니다. 문제는 UIProcess가 의도한 destination을 알고 있었음에도 NetworkProcess에는 bool만 반환했다는 점입니다. 그래서 NetworkProcess에는 claim을 보낸 쪽과 비교할 기준값이 없었습니다.
UIProcess NetworkProcess WebContent A WebContent B
───────── ────────────── ──────────── ────────────
choose dest = B ◄──── park loader L (key n)
reply(bool) ─────────► map[n] = L (no owner recorded)
ScheduleResourceLoad(resume=n)
take(n) ──► L bound to A ◄─────────────┘ (pre-fix: no check)
ScheduleResourceLoad(resume=n)
take(n) ──► NotFound ◄──────────────────────────────────────┘
After: take(n, caller) compares caller to recorded dest ─► WrongCaller ─► MESSAGE_CHECK
scheduleResourceLoad에도 앞단의 검증은 존재합니다. First-party-for-cookies에 대한 MESSAGE_CHECK와, webPageProxyID allow-list에 없으면 요청을 버리는 처리가 그것입니다. 다만 이 검증들은 claim을 보낸 쪽의 request parameter 자체만 확인할 뿐, park된 loader와의 관계는 확인하지 않습니다. Regression test는 자기 page의 URL과 webPageProxyIdentifierForMessageArguments만으로 이 검증을 통과합니다. Compromised renderer도 같은 방식으로 통과할 수 있음을 시사하는 대목입니다.
Identifier의 타입은 NetworkResourceLoadIdentifier입니다. NetworkResourceLoader constructor는 NetworkResourceLoadIdentifier::generate()로 이 값을 얻습니다. 이 함수는 WTF ObjectIdentifier를 순차적으로 생성하므로 값을 추측할 수 있습니다. addLoaderAwaitingWebProcessTransfer가 사용하는 map key도 이 생성된 ID일 가능성이 높습니다. 다만 identifier를 도출하는 호출 지점은 제공된 context에 포함되어 있지 않습니다. 이 추정이 맞다면, 같은 NetworkSession에 연결된 WebContent process는 parking window 안에서 올바른 identifier를 지정하기만 해도 loader를 넘겨받게 될 수 있습니다. Parking window는 최대 cachedNetworkResourceLoaderLifetime까지입니다. 이 경우 UIProcess가 지정한 process 대신 해당 process가 response 전달 대상이 될 가능성이 있습니다.
이번 fix로 identifier는 capability가 아니라 단순한 lookup key로 바뀌었습니다. 기록된 destination은 m_webProcessIdentifier와 비교됩니다. 이 값은 NetworkConnectionToWebProcess::create()에 전달되어 생성 시점에 저장됩니다. Connection이 유지되는 동안 고정되어 있고, WebContent process가 이후 message에서 임의로 지정할 수도 없습니다. 따라서 claim을 보낸 쪽의 identity로 신뢰할 수 있습니다. 한편 단순히 find 후 비교만 했다면, loader가 park된 시점부터 UIProcess가 응답하기까지의 window가 그대로 남았을 것입니다. 이른 claim을 바로 허용하지 않고 queue에 보관하는 방식이 이 window를 닫습니다.
Exploitability 측면에서 보면, 먼저 WebContent process에서의 code execution이 전제 조건입니다. 해당 process는 피해자와 같은 NetworkSession을 공유해야 하고, 임의의 ScheduleResourceLoad message를 보낼 수 있어야 합니다. Script만으로는 이 버그에 도달할 수 없습니다. 얻을 수 있는 primitive는 memory corruption이 아니라 process 간 data confidentiality와 integrity 침해입니다. 즉 claim을 보낸 쪽이 다른 navigation의 살아 있는 loader 소유권을 가져가는 형태입니다. 영향 범위의 상한은 같은 session에서, parking window 동안, swap이 진행 중인 main-frame navigation response를 위해 park된 loader로 한정됩니다.
transferToNewWebProcess가 response 전달 경로를 새 connection으로 다시 연결한다고 가정할 수 있습니다. 함수 이름과 m_networkResourceLoaders 등록 방식으로 볼 때 그렇게 추정됩니다. 이 가정이 성립하면 claim을 보낸 쪽이 해당 response의 header와 body를 받게 되고, 정상 destination은 load를 잃게 될 수 있습니다. 다만 이 함수의 본문은 제공된 context에 포함되어 있지 않으므로, 실제로 어떤 데이터가 전달되는지는 추론에 해당합니다. 일반적인 response 전달 외에 credential이 담긴 state가 추가로 넘어가는지는 아직 열려 있는 점검 포인트입니다. Commit message에는 후속 작업으로 map key를 추측하기 어렵게 만들 예정이라고 적혀 있습니다. Defense-in-depth 계층으로는 적절한 방향이지만, ownership check를 대체할 수는 없습니다.
한편 이번 패치로 새로 도달 가능한 상태도 하나 추가되었습니다. Park된 loader의 destination이 아직 정해지지 않은 동안에는, session에 연결된 어떤 WebContent process든 m_pendingClaims에 항목을 추가할 수 있습니다. 4개라는 상한은 모든 process가 공유합니다. 그리고 queue가 가득 차면, 그 시점에 한도에 걸린 connection에서 MESSAGE_CHECK(false)가 호출됩니다. Compromised process가 정상 destination의 claim보다 먼저 슬롯을 채운 경우를 가정할 수 있습니다. 이 경우 종료되는 쪽은 정상 process가 되며, 범위가 좁은 cross-process denial of service로 이어질 가능성이 있습니다. 이때 공격자가 queue에 넣은 claim도 setParkedLoaderDestinationAndResolvePendingClaims가 실행되는 시점에 함께 종료 처리될 것입니다. 두 testing message는 ENABLE(IPC_TESTING_API)로 gate되어 있으므로 production build에는 존재하지 않아야 합니다.
이 vulnerability는 NetworkProcess가 중개하는 WebContent process 간 cross-process isolation을 약화시킵니다. Navigation response 시점의 process swap은 UIProcess가 선택한 process만 park된 response를 받는다는 전제 위에서 동작합니다. 그러나 패치 이전의 NetworkProcess는 이 전제를 강제하지 않았습니다. 그 결과 compromised process가 다른 navigation의 진행 중인 main-resource loader를 가로챌 수 있는 상태였습니다. 다른 process, 경우에 따라서는 cross-site process로 향해야 할 response를 대신 받을 수 있었던 셈입니다. BrowsingContextGroup과 Enhanced-Security 분리가 제공하려던 confidentiality가 무력화될 수 있는 이유입니다.
Takeaway: IPC로 전달되는 identifier는 lookup key일 뿐, 결코 authorization token이 아닙니다. 리소스는 권한 주체가 선택한 principal에 묶어 두고, 모든 claim마다 connection에 고정된 identity를 확인해야 합니다.
Audit directions
-
Identifier를 credential처럼 취급하는 조회. Privileged broker가 신뢰할 수 없는 client에게서 받은 identifier로 공유 registry를 조회하는 패턴입니다. 이때 저장된 소유자가 호출자의 connection-bound identity와 일치하는지는 확인하지 않습니다. 좁은 범위:
*Identifier인자를 받는NetworkConnectionToWebProcesshandler를 검색합니다. 그중 connection이 아닌NetworkSession이나NetworkProcess소유의 map에find/take/get을 호출하는 곳이 대상입니다. Session 또는 process 단위로 조회한 뒤m_webProcessIdentifier와 비교하지 않고 바로 사용하는 코드가 판별 단서입니다. 넓은 범위: 추측 가능한 ID를 key로 쓰는 다른 cross-process registry도 살펴볼 필요가 있습니다. MessagePort entanglement(EntangleLocalPortInThisProcessToRemote), blob URL 등록,DownloadID,WebTransportSessionIdentifier, GPUProcessRemoteResourceCacheidentifier 등이 해당하며, 각각 소유자 단위의 binding이 있는지 확인해야 합니다. 가장 넓은 범위: 전형적인 IDOR / confused-deputy 계열입니다. Bound handle 대신base::UnguessableToken이나 정수 ID를 받는 Chromium Mojo interface, 정수 key 기반의 Android Binder service, REST API에서도 같은 유형이 나타납니다. 어디서든 판별 단서는 같습니다. Client보다 넓은 범위의 저장소에서 client가 제공한 ID로 리소스를 꺼내는 코드입니다. Code review에서도 IPC 인자가 곧바로 session 단위take()로 이어지는 handler라면 "누가 이 값을 지정할 수 있는가?"를 물어볼 필요가 있습니다. -
소유자가 확정되기 전의 대기 window. 정상 소유자가 확정되기 전에 리소스가 먼저 생성되면, 인증되지 않은 window가 남게 됩니다. 지켜야 할 invariant는 소유자가 미확정인 동안 리소스를 넘겨줄 수 없어야 한다는 것입니다. 지연된 claim은 queue에 보관하고, 소유자가 확정되는 시점에 다시 검증해야 합니다. 좁은 범위:
m_loadersAwaitingWebProcessTransfer에 남아 있는 검증 없는 accessor인takeParkedLoaderForOriginalProcess와removeLoaderWaitingWebProcessTransfer를 점검합니다. WebContent process에서 IPC로 이 함수들에 도달하는 경로가 없는지 확인해야 합니다. 넓은 범위: 한 IPC 교환에서 생성되고 이후 다른 교환에서 인계되는 NetworkProcess 객체도 대상입니다. Service-worker fetch-task 인계, keep-alive load 이전(transferKeptAliveLoad),ConvertMainResourceLoadToDownload등이 여기에 해당합니다. 인계받는 쪽에서 들어오는 입력이 identifier 하나뿐인 adopt/transfer method가 판별 단서입니다. 가장 넓은 범위: OAuth authorization-code 전달, 이름 기반 rendezvous를 통한 file descriptor 전달이 같은 범주에 속합니다. 2단계에서 먼저 나타난 쪽을 그대로 신뢰하는 모든 two-phase handshake도 마찬가지입니다. -
서로 신뢰하지 않는 주체 간의 공유 quota. 서로 신뢰하지 않는 주체들이 하나의 bounded queue를 공유하는 상황입니다. 이때 overflow가 발생하면 queue를 채운 주체가 아니라 한도에 걸린 주체가 불이익을 받습니다. 지켜야 할 invariant는 quota 고갈의 책임이 해당 quota를 사용한 주체에게 귀속되어야 한다는 것입니다. 좁은 범위:
CachedNetworkResourceLoader::maxPendingClaims와queuePendingLoaderClaim실패 시의MESSAGE_CHECK를 확인합니다. Destination이 아닌 process가 네 개의 슬롯을 채워, 정상 destination의 claim이 종료되도록 만들 수 있는지 따져봐야 합니다. 호출자별 집계 없이 상한만 확인하고, 현재 호출자에게 징벌적 조치를 취하는 코드가 판별 단서입니다. 넓은 범위: 여러 connection이 항목을 추가할 수 있는 리소스별 vector를 대상으로, NetworkProcess와 GPUProcess의 다른static constexpr상한을 검색합니다. 가장 넓은 범위: 공유 rate limiter, kernel의 객체별 waiter list, message broker의 topic별 quota에서 나타나는 multi-tenant fairness 문제와 같은 계열입니다. 이런 경우에는 tenant별 sub-quota가 표준적인 해법입니다.