← All reports

[2] UIProcess decodes WebContent-supplied image bytes outside the web-safe allow-list

MediumWebKit UIProcessSandboxEscape

Renderer bytes reached the host app's full codec set — PSD, RAW, EXR.

b2dca99

Medium으로 평가한 이유는 다음과 같습니다. 이번 patch는 memory safety 결함을 고치는 변경이 아니라, 무너져 있던 containment 경계를 복원하는 작업입니다. UIProcess의 세 경로가 renderer에서 넘어온 바이트를 플랫폼의 전체 codec 집합에 그대로 전달하고 있었습니다. 그 결과 PSD, OpenEXR, camera RAW, JPEG 2000의 parser 버그가 host application 자신의 프로세스 안에서, 침해된 renderer로부터 도달 가능한 상태였습니다.

브라우저 엔진은 image decoding을 좁은 범위 안에 묶어 둡니다. image parser는 코드 양이 많고 과거부터 버그가 잦았던 C 코드이기 때문입니다. 그래서 renderer와 GPU process는 web-safe allow-list에 올라 있는 포맷만 디코딩합니다. 다만 이 allow-list는 프로세스 단위 스위치가 아니라, UTIRegistry.h에 선언된 WebCore::isSupportedImageType()이라는 predicate 형태입니다. UIProcess에서는 프로세스 단위 스위치를 쓸 수도 없습니다. UIProcess 자체가 host application의 프로세스이고, 애플리케이션은 사용자가 고른 임의의 이미지 파일을 정상적인 동작으로 열기 때문입니다. 결국 WebContent에서 넘어온 바이트를 다루는 UIProcess의 모든 호출 지점에서, 이 invariant를 매번 직접 다시 세워야 합니다.

관전 포인트: 침해된 WebContent process가 host application의 프로세스를 플랫폼의 전체 image codec 집합 — PSD, OpenEXR, camera RAW, JPEG 2000 — 으로 유도할 수 있습니다. 경로는 사진 저장, share 메뉴 썸네일, attributed string 첨부입니다. renderer의 디코딩을 가둬 두던 sandbox 바깥에서 벌어지는 일입니다.

commit message에 따르면, 이번 변경은 이미지를 shared memory 바깥으로 복사한 뒤 지원되는 image type인 경우에만 photo library에 저장합니다.

수정 대상은 두 계층에 걸친 세 곳입니다. 먼저 renderer 바이트로 플랫폼 이미지 객체를 생성하는 Cocoa UIProcess 호출 지점 두 곳입니다. 나머지 하나는 file wrapper를 AppKit 또는 UIKit에 전달하는 WebCore의 attributed string 재구성 경로입니다. 세 곳 모두에 isSupportedImageType() predicate가 추가되었고, iOS 경로에서는 바이트를 얻는 방식 자체도 함께 재구성되었습니다.

iOS에서 PageClientImpl::saveImageToLibrary()는 SaveImageToLibrary(WebCore::SharedMemory::Handle handle, String authorizationToken) IPC 메시지를 통해 도달합니다. WebPageProxyIOS.mm이 handle을 매핑하고 createSharedBuffer()를 호출한 뒤 결과를 page client에 전달하는데, 패치 이전에는 이 raw 바이트가 UIImageDataWriteToSavedPhotosAlbum()으로 그대로 넘어갔습니다. 패치 이후에는 toNSData(imageBuffer->span())으로 바이트를 먼저 복사해 실체화한 다음, 그 복사본을 대상으로 포맷을 판별합니다. 이렇게 하면 type 검사와 photo library 쓰기가 동일한 버퍼를 대상으로 수행됩니다. 송신 프로세스가 여전히 쓰기 가능한 상태로 들고 있을 수 있는 매핑을 다시 읽지 않게 되는 셈입니다. macOS에서는 WebContextMenuProxyMac.mm이 hitTestData.imageSharedMemory 위에서 [[NSImage alloc] initWithData:...toNSData()]로 share 메뉴 썸네일을 생성하고 있었습니다. WebCore 쪽에서는 AttributedString::nsAttributedString()의 TextAttachmentFileWrapper 디코드 경로가 첨부의 raw 바이트를 NSTextAttachment를 뒷받침하는 NSFileWrapper에 그대로 넣었습니다. 이 데이터는 나중에 첨부가 렌더링되는 시점에 AppKit 또는 UIKit이 디코딩합니다. 새로 추가된 DropsUnsupportedImageData 테스트는 round-trip을 거친 첨부 내용이 원래의 Photoshop 바이트와 더 이상 같지 않음을 검증합니다.

  Before (iOS save path):                 After:
  SaveImageToLibrary(handle)              SaveImageToLibrary(handle)
    └─► map shared memory                   └─► map shared memory
          └─► createSharedBuffer()                └─► toNSData(span())   (copy out)
                └─► UIImageDataWrite...                 └─► isSupportedImageType?
                      └─ ImageIO selects                      ├─ no  ──► drop
                         ANY installed codec                  └─ yes ──► UIImageDataWrite...

프로세스 단위가 아니라 호출 지점 단위로 강제되는 containment allow-list, 그리고 경계를 넘어온 데이터를 다루면서 그 검사를 전혀 수행하지 않던 세 곳.

이 코드가 있는 위치. UIProcess는 host application 그 자체입니다. Safari이거나, WKWebView를 임베드한 임의의 애플리케이션입니다. WebContent나 GPU process처럼 sandbox에 갇혀 있지는 않습니다. 사용자가 고른 파일을 열고, window server와 통신하고, photo library에 기록하는 등 애플리케이션다운 동작을 수행해야 하기 때문입니다. WebContent에서 UIProcess로 들어오는 모든 바이트는, 문제가 되는 방향으로 권한 경계를 넘어옵니다.

Uniform Type Identifier와 포맷 판별. UTI는 데이터의 종류를 나타내는 플랫폼 표준 식별자입니다 (public.png, public.jpeg, com.adobe.photoshop-image). ImageIO는 호출자가 포맷을 명시하도록 요구하지 않습니다. 바이트 버퍼를 받으면 magic byte를 검사해 일치하는 설치된 codec을 스스로 선택합니다. 애플리케이션 입장에서는 편리한 동작이지만, trust boundary에서는 위험합니다. 어떤 parser가 실행될지 결정하는 주체가 호출자가 아니라 데이터이기 때문입니다.

web-safe 서브셋. WebCore::isSupportedImageType()은 web platform이 디코딩하기로 되어 있는 UTI 목록을 정의합니다. 반면 운영체제가 기본 제공하는 집합은 훨씬 넓습니다. Photoshop 문서, OpenEXR, camera RAW 계열, JPEG 2000 등이 모두 ImageIO로 디코딩 가능하지만, 일반적인 웹 페이지에서는 그중 어느 것도 도달할 수 없습니다. "이 플랫폼이 파싱할 수 있는 것"과 "웹이 필요로 하는 것" 사이의 간극, 그 간극이 바로 이 predicate가 존재하는 이유입니다.

attributed string과 file wrapper. AttributedString은 rich text 구간을 IPC로 직렬화하기 위한 WebKit 내부 표현입니다. 편집, 사전 조회, rich text 전송에 사용되며, nsAttributedString()이 이를 Cocoa 객체로 복원하는 쪽을 담당합니다. NSFileWrapper가 뒷받침하는 NSTextAttachment는 임의의 파일 내용을 그대로 담을 수 있습니다. 그 내용은 첨부가 실제로 그려지는 시점에 text system이 지연 디코딩합니다. 즉 바이트를 받아들인 deserialization 지점과 실제 파싱 지점이 멀리 떨어지게 됩니다.

이번 건은 memory safety 결함이 아닙니다. containment invariant가 아예 없었던 경우이고, 버그 유형으로는 sandbox 경계를 넘는 공격 표면 노출에 해당합니다. 세 경로 모두에서 최종적으로 실행될 decoder는, 공격자가 제공한 바이트를 ImageIO의 포맷 판별기가 훑어본 결과로 선택됩니다. 그래서 도달 가능한 parser 집합이 web-safe 서브셋이 아니라 설치된 전체 codec 목록으로 넓어집니다. commit message는 예시로 PSD, OpenEXR, camera RAW, JPEG 2000을 언급합니다. 여기에 더해 DropsUnsupportedImageData 테스트는 round-trip을 거친 첨부 내용이 원래 PSD 바이트와 달라졌음을 검증하므로, Photoshop(8BPS) 데이터가 allow-list 밖에 있다는 사실이 별도로 확인됩니다.

세 경로 중 가장 눈에 띄지 않으면서 점검하는 입장에서 가장 흥미로운 쪽은 attributed string 경로입니다. 데이터를 받아들이는 지점과 디코딩이 일어나는 지점이 다르기 때문입니다. nsAttributedString()은 NSFileWrapper를 구성하는 데서 끝납니다. 실제 파싱은 이후 AppKit 또는 UIKit이 첨부를 렌더링하는 시점에 발생합니다. 그래서 deserialization 코드만 들여다보는 리뷰어에게는 decoder 호출이 전혀 보이지 않습니다.

iOS 쪽 구조 변경은 allow-list 자체와는 별개로 짚어 둘 만합니다. shared memory 위에서 type을 검사한 뒤 같은 매핑에서 데이터를 읽어 쓰는 방식은, 검사 시점과 사용 시점이 분리된 구조입니다. 송신 프로세스가 해당 영역을 여전히 쓰기 가능한 상태로 들고 있을 수 있습니다. 그러면 isSupportedImageType()을 통과한 바이트와 UIImageDataWriteToSavedPhotosAlbum()에 도달하는 바이트가 서로 다를 수 있습니다. 포맷 판별 전에 toNSData(imageBuffer->span())으로 NSData 복사본을 만들어 두면, 두 번의 관찰이 하나의 버퍼로 모이게 됩니다. 공유 매핑 위에서 검증 후 사용하는 순서를 다룰 때 올바른 형태입니다.

attacker가 얻는 것은 도달 가능성까지입니다. 이번 변경으로 renderer가 제어하는 바이트가 host application 프로세스 안에서 설치된 임의의 codec을 구동할 수 있었다는 점이 드러납니다. 다만 그 자체로 memory safety primitive가 성립하지는 않습니다. primitive가 되려면 해당 codec 중 하나에 버그가 있어야 하기 때문입니다. 여기서의 가치는 공격 표면의 변화량이고, 그 폭이 상당히 큽니다. ImageIO parser 버그의 값어치가 가장 높은 곳이 바로 UIProcess입니다. 애플리케이션의 entitlement를 보유한 프로세스이기 때문입니다.

패치 이전에는 multi-process 설계가 의도한 image decoding containment가 renderer와 GPU process에서는 유지되고 있었습니다. 다만 UIProcess의 IPC 경계에서는 지켜지지 않았습니다. 하필 codec exploit이 성공했을 때 가치가 가장 큰 지점입니다.