[4] WebPasteboardProxy accepted remote frame IDs outside the copied subtree
A frame ID is a name, not a permission — the pasteboard disagreed.
Medium 등급입니다. Site Isolation이 지키려는 경계가 바로 이 취약점으로 깨집니다. 손상된 renderer가 피해자 프로세스에서 별도의 memory-safety 버그 없이도 다른 사이트의 직렬화된 DOM을 얻을 수 있습니다. 다만 사전 compromise가 전제 조건이고 결과도 disclosure에 그치며 memory-corruption primitive로 이어지지 않기 때문에 High band에는 들지 않습니다.
Site Isolation 환경에서는 서로 다른 사이트의 frame이 각기 별도의 content process에 호스팅되므로, 페이지 전체의 DOM을 한 프로세스가 모두 갖고 있지 않습니다. 이런 분할 경계를 넘나드는 페이지를 복사하려면 UI process가 aggregator 역할을 맡아야 합니다. 전송하는 프로세스는 자신이 로컬로 갖고 있는 frame들의 archive와 out-of-process frame들의 식별자 목록을 제출하고, UI process가 나머지를 모아옵니다. FrameIdentifier는 프로세스 전역 handle로, UI process가 브라우저의 모든 frame을 아우르는 registry를 통해 이를 resolve할 수 있습니다. 여기서 지켜져야 할 보장은, 한 프로세스가 이 aggregator를 통해 접근할 수 있는 frame이 자신이 실제로 복사 중인 subtree에 한정되어야 한다는 점입니다.
관전 포인트: 손상된 renderer가 브라우저 안의 살아있는 아무 frame이나 자신의 복사 작업의 remote subframe인 것처럼 지정하면, UI process는 그 frame의 DOM을 attacker가 지정한 pasteboard에 직렬화해 써넣게 됩니다.
기존 WebPasteboardProxy는 전달받은 remote frame ID에 대해 subtree 검사를 전혀 수행하지 않았습니다. 그 결과 무관한 cross-site remote frame의 콘텐츠가 pasteboard에 기록될 수 있었습니다. 이를 해결하기 위해, WebPasteboardProxy 내부에서 사용되는 모든 remote frame ID가 전달된 root frame ID의 하위 노드인지 확인하는 message check가 추가되었습니다.
Source/WebKit/UIProcess/Cocoa/WebPasteboardProxyCocoa.mm
LayoutTests/http/tests/ipc/resources/write-web-archive-frame-ancestry-check-iframe.html
Patch Details
이번 변경은 ancestry validator를 새로 추가하고 이를 두 pasteboard-write 진입점 모두에 연결한 뒤, 거부 여부가 테스트에서 관찰 가능하도록 sync reply 구조를 다시 짰습니다.
새로 추가된 file-static validateFrameIdentifiers()는 두 개의 lambda로 구성됩니다. isInSubtree는 WebFrameProxy::parentFrame()을 따라 위로 올라가며 root를 찾고, isAllowed는 해당 identifier가 root와 같거나, WebFrameProxy::webFrame(identifier)가 아무것도 resolve하지 못하거나, resolve된 frame이 root의 subtree 안에 있을 때 이를 허용합니다. pasteboard 경로에 도달하는 모든 identifier가 이 검증을 거칩니다. localFrameArchives의 각 key, 각 archive 자신의 frameIdentifier(), archive의 subframeIdentifiers() 각 항목, 그리고 remoteFrameIdentifiers의 모든 항목이 대상입니다. writeWebContentToPasteboard에는 createOneWebArchiveFromFrames 호출 직전에 MESSAGE_CHECK가 추가되었고, writeWebArchiveToPasteBoard에는 MESSAGE_CHECK_COMPLETION이 추가되었습니다.
거부 여부를 구분할 수 있도록, WriteWebArchiveToPasteBoard의 sync reply가 (int64_t changeCount)에서 (enum:uint8_t WriteWebArchiveToPasteBoardResult result, int64_t changeCount)로 바뀌었습니다. 이를 위해 새로운 Shared/WriteWebArchiveToPasteBoardResult.h와 .serialization.in이 추가되어 Success / FailureDueToInvalidFrameIdentifiers / FailureOther를 정의합니다. handler 내부의 모든 early return과 그 안에 중첩된 completion lambda들이 이제 result code를 함께 전달하며, WebPlatformStrategies::writeWebArchive는 두 값짜리 reply를 destructure하되 newChangeCount만 반환하도록 유지됩니다. 부수적으로 새 serialization 파일에 대한 빌드 시스템 등록이 이루어졌고, RemotePageProxy::RemotePageProxy에 테스트 전용 hook이 추가되어 ENABLE(IPC_TESTING_API) 하에서 ipcTestingAPIEnabled()와 ignoreInvalidMessageWhenIPCTestingAPIEnabled() preference가 모두 설정된 경우 setIgnoreInvalidMessageForTesting()을 호출합니다. 이를 통해 layout test가 site-isolated subframe process에서 의도적으로 invalid한 message를 보내더라도 해당 프로세스가 강제 종료되지 않도록 합니다.
호출자가 넘긴 global identifier를 그 자체로 entitlement의 증거로 신뢰하며, 해당 객체가 호출자에게 권한이 있는 subtree 안에 있는지 확인하지 않는 패턴입니다.
Background
Site Isolation. 서로 다른 사이트의 frame은 각각 별도의 WebContent process에 호스팅됩니다. 하나의 process는 자신의 frame에 대한 DOM만 갖고 있으며, cross-site subframe은 로컬에서는 placeholder 형태의 "remote frame"으로 나타납니다.
FrameIdentifier. frame을 가리키는 프로세스 전역 identifier입니다. UI process에서는 WebFrameProxy::webFrame(identifier)가 어떤 identifier든 해당 WebFrameProxy로 resolve할 수 있고, WebFrameProxy::parentFrame()은 UI process가 모든 WebContent process를 아울러 관리하는 authoritative frame tree를 따라 이동합니다.
LegacyWebArchive. WebKit의 직렬화된 페이지 포맷으로, main resource와 subresource, 중첩된 subframe archive로 구성됩니다. 자신이 온 frame의 identifier(frameIdentifier())와 자식 frame들의 identifier(subframeIdentifiers())를 함께 기록하므로, IPC로 전달되는 archive는 message의 top-level vector뿐 아니라 archive 내부에도 frame identifier를 담고 있습니다.
Multi-process archive assembly. 복사 대상이 site-isolated frame tree에 걸쳐 있을 경우, 전송하는 프로세스는 자신의 로컬 frame에 대한 archive와 out-of-process frame들의 FrameIdentifier 목록을 함께 제출합니다. 나머지 조각들은 WebPasteboardProxy::createOneWebArchiveFromFrames가 모읍니다. LegacyWebArchiveCallbackAggregator는 addResult()를 통해 이들을 수집하고, 소멸 시점에 completeFrameArchive(rootFrameIdentifier)를 호출해 subframe archive들을 상위 archive에 결합한 뒤 completion handler를 호출합니다.
MESSAGE_CHECK / MESSAGE_CHECK_COMPLETION. WebKit의 IPC 검증 macro입니다. 검사가 실패하면 해당 message는 malformed로 취급되어 보낸 쪽 connection이 종료됩니다. _COMPLETION variant는 sync reply가 유실되지 않도록 먼저 전달된 completion expression을 실행합니다.
ENABLE(IPC_TESTING_API). 빌드 및 preference로 gate되는 기능으로, layout test에서 JavaScript에 IPC 객체를 노출시켜 손상된 WebContent process를 흉내낼 수 있게 합니다. setIgnoreInvalidMessageForTesting()은 MESSAGE_CHECK 실패로 인해 테스트 프로세스가 강제 종료되는 것을 막습니다.
Pasteboard naming. pasteboard write는 호출자가 지정한 pasteboardName 문자열로 키가 결정되며, WebPasteboardProxy는 WebContent process를 위한 read 측 기능도 함께 제공합니다.
Analysis
이 취약점은 IPC로 전달된 identifier에 대한 authorization 누락입니다. memory-safety 버그가 아니라 confused-deputy 성격의 site-isolation bypass이며, 결과적으로 cross-origin information disclosure로 이어집니다.
WebContent A (127.0.0.1) WebContent B (localhost)
------------------------ ------------------------
main frame [F1] iframe [F2] (child of F1)
|
| WriteWebArchiveToPasteBoard
| root = F2, remote = [F1]
v
UI process: WebPasteboardProxy
pre-fix : webFrame(F1) resolves -> archive F1 -> pasteboard
post-fix: isInSubtree(F1, root=F2) == false -> reject
패치 이전에 이루어지던 검증은 pasteboard 이름이 비어 있지 않은지, 그리고 webFrame(rootFrameIdentifier)가 null이 아닌지 확인하는 정도였습니다. FrameIdentifier가 전역 registry를 통해 resolve되기 때문에, sender가 message에 적어 넣은 identifier는 그것을 보낸 프로세스나 frame tree상 위치와 무관하게 브라우저 어딘가에 있는 frame으로 resolve됩니다. 위 다이어그램에서 볼 수 있듯, attacker가 지정한 identifier는 선언된 root의 조상일 수도, sibling일 수도 있습니다. 심지어 지정된 frame이 같은 페이지에 속해야 한다는 제약도 없었으므로, 완전히 다른 tab의 frame일 수도 있었습니다. 이 경우 createOneWebArchiveFromFrames는 해당 frame을 resolve해 그 직렬화된 DOM을 attacker가 지정한 pasteboard에 기록되는 archive에 병합했습니다.
이 경로는 일반적인 웹 콘텐츠에서는 도달할 수 없습니다. JavaScript에는 rootFrameIdentifier나 remoteFrameIdentifiers vector를 직접 선택할 방법이 없으며, 정상적인 producer인 WebPlatformStrategies::writeWebArchive는 두 값 모두 실제로 복사 중인 frame tree로부터 도출합니다. 따라서 이 취약한 상태에 도달하려면 WebContent→UIProcess connection 상에서 임의의 IPC를 보낼 수 있는 능력이 필요합니다. layout test에서는 이 능력을 IPCTestingAPIEnabled=true 설정으로 대체합니다.
이런 능력이 주어졌다는 전제 하에서는, 공격에 별도의 memory grooming이 필요하지 않습니다. attacker가 임의로 정한 pasteboardName, 손상된 process가 정당하게 소유한 frame으로 설정한 rootFrameIdentifier, 빈 localFrameArchives map, 그리고 cross-site 피해자 frame의 identifier를 담은 remoteFrameIdentifiers를 넣어 WriteWebArchiveToPasteBoard를 보내면 됩니다. regression test도 정확히 이 형태를 따릅니다. 127.0.0.1:8000의 최상위 문서가 localhost:8000 iframe을 embed하고 있는데, Site Isolation 하에서 이 iframe은 다른 process에 위치하게 됩니다. iframe은 postMessage를 통해 부모에게 IPC.frameID[0]을 요청한 뒤, sync message를 [String pbName, FrameID(myFrameID), u64(0), u64(1), FrameID(parentFrameID)] 형태로 직접 인코딩해 보냅니다. 여기서 지정된 remote frame은 선언된 root의 조상에 해당하므로, isInSubtree는 parent로부터 위쪽으로 탐색하며 child의 frameID()를 결코 만나지 못합니다. 결과적으로 isAllowed는 false를 반환하고, handler는 cross-process archive를 조립하는 대신 FailureDueToInvalidFrameIdentifiers를 응답하게 됩니다.
결과를 읽어오는 부분은 별도로 gate되어 있습니다. WebPasteboardProxy::accessType()/canAccessPasteboardData()는 사전에 이루어진 grantAccess() 호출이나, 해당 process 안의 페이지가 domPasteAllowed()와 javaScriptCanAccessClipboard()를 모두 만족하는지에 의존합니다. 따라서 archive를 process 내부에서 다시 읽어오려면 이 gate를 통과해야 합니다. 반면 system pasteboard로의 disclosure, 그리고 사용자가 이후 어딘가에 붙여넣는 시점까지 이어지는 유출 자체는 이 gate의 영향을 받지 않습니다. Escalation은 disclosure 이상으로 이어지지 않는 것으로 보입니다. attacker가 제어할 수 있는 length, offset, buffer-size 필드가 전혀 없으므로, 이 취약점이 memory-corruption primitive로 전환될 가능성은 확인되지 않습니다.
이 취약점은 UI process가 지켜야 할 Site Isolation trust boundary를 약화시킵니다. 여기서 지켜져야 할 전제는, WebContent process가 UI process로 하여금 자신이 정당하게 호스팅하거나 복사 중인 frame의 하위에 있는 frame에 대해서만 동작하도록 만들 수 있다는 것입니다. 패치 이전에는 신뢰할 수 없는 sender가 보낸 raw FrameIdentifier가 브라우저 전역 registry의 어떤 frame에든 도달할 수 있는 충분한 entitlement로 취급되었습니다. 한 WebContent process에서 code execution을 확보한 attacker라면, 피해자 process에서 별도의 memory-safety 버그를 트리거하지 않고도 UI process가 무관한 cross-site frame의 DOM을 직렬화하도록 만들 수 있었습니다. 이는 그 frame에 렌더링된 콘텐츠와 그 안의 각종 secret을 포함합니다.
Insight: 이 취약점은 Site Isolation retrofit에서 전형적으로 나타나는 형태의 버그입니다. isolation 도입 이전부터 존재하던 IPC message가 FrameIdentifier를 순수한 데이터로 실어 날랐는데, 이 message가 multi-process frame tree를 기술하도록 확장되면서 identifier들이 대응하는 entitlement check를 얻지 못한 채 그 자체로 capability가 되어버린 셈입니다. 이번 fix에서 눈여겨볼 지점이 두 가지 있습니다. 먼저, 검증 대상이 message signature에 명시된 identifier뿐 아니라 직렬화된 payload 내부에 중첩된 identifier까지 포함합니다. 이는 completeFrameArchive가 재귀적으로 subframeIdentifiers()를 확장해 더 많은 frame을 끌어오기 때문에 필요한 조치입니다. 또한 isAllowed는 UI process가 resolve할 수 없는 identifier에 대해서는 fail-open으로 동작합니다 (return !frame || isInSubtree(*frame)). 즉 실제로 제공되는 보장은 "살아 있고 resolve 가능하며 subtree 밖에 있는 frame은 끌어올 수 없다"는 것이지, "subtree 안의 frame만 지정할 수 있다"는 더 강한 보장은 아닙니다. commit message의 표현보다는 다소 약한 post-condition이며, teardown race를 함께 고려할 때 유의할 부분입니다.
Audit directions
-
호출자가 지정한 global identifier를 process-wide registry를 통해 resolve하고, resolve 성공 자체를 entitlement의 증명으로 취급하는 IPC handler. Narrow:
Source/WebKit/UIProcess에서.messages.in에 선언된 handler 내부의WebFrameProxy::webFrame(,WebProcessProxy::webPage(,WebProcessProxy::webFrame(을 grep하고, resolve된 객체가WebProcessProxy::fromConnection(connection)과 연결되는지 하나씩 확인해야 합니다. Wider: identifier가 connection별 handle이 아니라 global table을 가리키는 bare integer key인 경우 어디서든 같은 class의 문제가 나타납니다. GPUProcess/ModelProcess의RemoteXXXidentifier map과, sender가 제어하는ObjectIdentifier로 키가 매겨지는NetworkConnectionToWebProcesshandler의 lookup을 점검해야 합니다. Widest: 이름을 갖고 있다는 사실이 capability를 갖고 있다는 뜻은 아닙니다. Routing ID, handle, resource ID가 peer별이 아니라 global namespace로 관리되는 모든 multi-process/multi-tenant system에 이 원칙을 적용해야 합니다 — Mojo routing ID, client가 제공한 resource ID로 키가 매겨지는 gRPC service, ACL 평가보다 먼저 opaque ID로 fetch하는 cloud API 등이 해당됩니다. Code-review tell:FrameIdentifier나WebPageProxyIdentifier파라미터를 받아 dereference하면서, 근처에 sending connection과 연결짓는MESSAGE_CHECK가 없는 handler입니다. -
Message signature에 이름이 명시된 identifier에는 validation이 적용되지만, message가 담고 있는 serialized object 내부에 embedding된 identifier에는 validation이 적용되지 않는 패턴이며, 이후 expansion 단계에서 embedded graph를 순회합니다. Narrow:
WebPasteboardProxyCocoa.mm과LegacyWebArchiveCallbackAggregator.h에서LegacyWebArchive::subframeIdentifiers()와frameIdentifier()의 모든 consumer를 추적하고, 각 expansion 지점이validateFrameIdentifiers이후에 위치하는지 확인해야 합니다. Wider: deserialize된 형태 안에 나중에 resolve되는 또 다른 identifier가 들어 있는 다른 IPC type도 점검해야 합니다 —DragData/PasteboardWebContentpayload,WebsitePoliciesData, 그리고ObjectIdentifier필드를 가진.serialization.instruct 전반이 대상입니다. Widest: payload의 첫 번째 층위만이 아니라 transitive closure 전체를 validate해야 합니다. 같은 형태의 문제가 nested-JSON authorization bypass, GraphQL nested-resolver의 ACL gap, manifest만 validate하고 entry별 경로는 validate하지 않는 archive extraction에서도 발생합니다. Code-review tell: element type에 identifier 타입 member를 가진 container 파라미터 바로 옆에, top-level 파라미터에만MESSAGE_CHECK가 걸려 있는 경우입니다. -
Fail-open branch를 검증해야 합니다.
WebFrameProxy::webFrame(identifier)가 아무것도 resolve하지 못하면isAllowed는 true를 반환합니다. Narrow: WebContent process가 mid-navigation, mid-swap, 또는RemotePageProxysetup/teardown 도중 UI-process registry에서 attacker 입장에서 의미 있는 live frame을 일시적으로 unresolvable 상태로 만들 수 있는지, 그리고 validation이 true를 반환한 뒤에도createOneWebArchiveFromFrames가 해당 frame의 content를 여전히 얻을 수 있는지를 실험적으로 확인해야 합니다. 이 부분은 site-isolated navigation을 이용한 runtime 실험이 필요하며, grep만으로는 확인되지 않습니다. Wider: UIProcess의 validator에서!obj ||나if (!x) return true;형태를 포함하는,MESSAGE_CHECK인접 predicate를 검색해야 합니다. Widest: unknown 상태는 allow가 아니라 deny로 매핑되어야 합니다. 같은 invariant가 resolve 불가능한 경로를 포기하는 path canonicalization이나, subject가 없을 때 검사를 건너뛰는 capability check에도 적용됩니다. Code-review tell: lookup 실패가 reject branch가 아니라 allow branch 안으로 접혀 들어간 형태입니다. -
요청한 process가 참여자를 직접 선택해서, 다른 content process들로 요청을 fan-out하는 UI-process operation입니다.
createOneWebArchiveFromFrames의 나머지 caller를 점검해야 합니다. 현재 두 곳 모두 guard가 걸려 있으므로, 세 번째 caller가 새로 추가되지 않았는지 확인해야 합니다. 이어서UIProcess내 다른 곳에서도 같은 fan-out 형태를 찾아야 합니다.Vector<FrameIdentifier>나 process/page identifier의 collection을 받아 element마다 message를 dispatch하는 handler가 대상이며, 각 element는 다른 process의 상태를 암묵적으로 읽는 행위에 해당하기 때문입니다. Wider: drag-and-drop, print/snapshot, find-in-page, accessibility 경로까지 범위를 넓혀야 합니다. 이들 경로는 Site Isolation을 위해 cross-process aggregation을 추가해야 했고, aggregation set이 sender에 의해 결정된다는 동일한 위험을 안고 있습니다. Widest: trusted broker가 third party로부터 데이터를 읽게 만드는 요청은 requester의 목록이 아니라 requester의 authority 범위 내로 scope되어야 합니다. 같은 invariant가 client가 지정한 upstream을 갖는 service mesh나, caller가 resource ID를 나열하는 batch endpoint에도 적용됩니다. Code-review tell: IPC로 전달된 container를 순회하는 loop 안에WebFrameProxy/RemotePageProxylookup이 위치한 형태입니다.