← All reports

[3] `WebPageProxy::didFailLoadForFrame` accepts an unvalidated failing URL

HighWebKit UI processAuthBypass

A compromised renderer names a file it can't read, and Safari hands it back.

248f89a

High로 평가한 이유: 이 취약점의 chain 전체에는 memory corruption이 전혀 없습니다. 다만 결과적으로 file:// 경로에 대한 read grant가, 원래 그런 권한을 가진 적이 없는 WebContent process에까지 확장됩니다. 이 확장이 성립하려면 renderer가 이미 compromise된 상태여야 하고, 동시에 embedder가 실패한 URL을 alternate-HTML API를 통해 그대로 반사시켜야 합니다. 바로 MobileSafari가 이런 방식으로 동작합니다.

WebKit은 web content를 sandbox 처리된 process에서 실행하며, UI process는 이 process를 신뢰하지 않는 대상으로 취급합니다. 이 경계를 넘어 위쪽으로 전달되는 모든 URL은, 해당 process가 실제로 부여받은 URL 집합과 대조해 검증되어야 합니다. WebPageProxy는 UI process 측에서 하나의 page를 대표하는 객체로, WebContent process로부터 load 진행 상황과 load 실패 메시지를 받아 embedding app의 navigation client 호출로 변환합니다. 이런 검증을 담당하는 관용구가 MESSAGE_CHECK_URL입니다. 이 매크로는 URL을 보낸 process의 허용된 집합과 대조 검증하고, 불일치하면 connection을 종료합니다. Privileged 동작에 영향을 줄 수 있는 WebContent 제공 URL이라면 사용 전에 반드시 이 검증을 거쳐야 합니다.

관전 포인트: compromise된 WebContent process가 임의의 file:// URL을 지정해 load 실패를 보고할 수 있습니다. Embedder가 이 URL을 alternate-HTML API를 통해 그대로 반사시키면, UI process와 Network process 양쪽에서 해당 디렉터리에 대한 read access를 얻게 됩니다.

WebPageProxy::didFailLoadForFrame은 이미 frame과 frameInfo를 검증하고 있었고, error의 domain과 code도 기록하고 있었습니다. 이번 패치는 WebCore::ResourceErrorm_navigationClient->didFailNavigationWithError / loader client로 전달되기 전에 MESSAGE_CHECK_URL(process, error.failingURL())을 추가합니다. 이로써 형제 entry point인 didFailProvisionalLoadForFrameShared가 동일한 필드에 대해 이미 수행하던 검증과 동일한 수준을 갖추게 되었습니다.

한쪽 entry point에서는 untrusted input이 검증되지만 형제 entry point에서는 검증되지 않고, 이후 해당 필드를 embedder가 승인한 것으로 취급하는 API를 거쳐 다시 세탁되어 돌아오는 패턴입니다.

Process 모델. WebKit은 작업을 네 가지 process 역할로 분리합니다. WebContent(신뢰하지 않는 page content를 실행하며 sandbox 처리됨), GPU, Networking, UI가 그것입니다. UI process는 embedding application을 host하며 sandbox 정책의 권한을 갖는 주체이기도 합니다. 즉 filesystem 접근 권한을 부여하는 sandbox extension을 내주는 process가 바로 UI process입니다.

WebPageProxy와 IPC endpoint. 각 web page는 UI process 안에 하나의 WebPageProxy를 갖습니다. 이 객체의 message handler는 WebContent process에서 직접 도달 가능하기 때문에, renderer가 제공한 데이터를 검증하는 주요 길목 역할을 합니다.

MESSAGE_CHECKMESSAGE_CHECK_URL. 이 매크로들은 trust boundary를 코드로 표현합니다. MESSAGE_CHECK는 IPC로 전달된 데이터의 invariant가 깨지면 connection을 종료하고, MESSAGE_CHECK_URL은 이를 특화해 URL을 보낸 process가 부여받은 집합과 대조 검증합니다. 이런 매크로가 존재하는 이유는, 검증 실패를 단순히 기록되는 이상 징후가 아니라 되돌릴 수 없는 확정적 결과로 만들기 위해서입니다. compromise된 renderer는 fallback 경로를 얻는 대신 connection을 잃어야 합니다.

IPC를 통한 ResourceError. WebCore::ResourceError는 완전히 serializable하며, ResourceError::fromIPCData / CoreIPCError를 통해 IPC payload로부터 그대로 재구성됩니다. 이 객체의 failingURL()URL 타입 필드로, 그 내용은 sender가 기록한 값 그대로입니다.

Alternate HTML. -[WKWebView _loadAlternateHTMLString:baseURL:forUnreachableURL:]는 embedder가 실패한 load 대신 error page를 표시할 때 사용하는 API입니다. 이 호출은 WebPageProxy::loadAlternateHTML에 도달하며, base/unreachable URL을 embedder가 승인한 값으로 취급합니다.

이번 버그는 IPC input validation이 누락된 사례입니다. Memory-safety 문제가 아니라 trust-boundary와 capability confusion에 해당하는 logic 결함입니다. 지켜지지 않은 invariant는 UI process에 적용되는 WebKit의 표준 규칙입니다. Privileged 동작에 영향을 줄 수 있는, WebContent process에서 비롯된 모든 URL은 사용되거나 전달되기 전에 해당 process가 부여받은 URL 집합을 기준으로 MESSAGE_CHECK_URL을 통과해야 합니다. 문제는 두 load-failure 경로 사이의 커버리지가 비대칭적이었다는 점입니다. Provisional 경로에서는 이 검증이 적용되어 있었지만, committed 경로에서는 적용되어 있지 않았습니다.

  WebContent (sandboxed)        UI process                    Network process
  ----------------------        ----------                    ---------------
  DidFailLoadForFrame
    error.failingURL =
      "file:///Users/..."  --->  didFailLoadForFrame
                                   [no MESSAGE_CHECK_URL]
                                        |
                                        v
                                 navigationClient->
                                   didFailNavigationWithError
                                        |  (NSError, embedder-trusted)
                                        v
                                 embedder re-enters:
                                   _loadAlternateHTMLString:
                                     forUnreachableURL:
                                        |
                                        v
                                 loadAlternateHTML --> grants dir read ---> same grant

이 laundering 자체가 exploit의 핵심입니다. failingURL은 신뢰할 수 없는 WebContent 제공 데이터로 UI process에 들어와서, WebKit이 내보내는 NSError / WKNavigationDelegate 인자로 빠져나갑니다. Embedder 입장에서는 이 값을 권위 있는 것으로 취급하는 편이 합리적인데, UI process가 이 인터페이스를 통해 내보내는 다른 모든 값이 실제로 그렇기 때문입니다. Commit message는 iOS에서의 구체적인 loop를 명시합니다. MobileSafari는 delegate callback에서 받은 failingURL을 그대로 다시 forUnreachableURL:-[WKWebView _loadAlternateHTMLString:baseURL:forUnreachableURL:]에 전달하고, 이 호출은 WebPageProxy::loadAlternateHTML에 도달합니다.

해당 API는 base와 unreachable URL을 embedder가 승인한 값으로 취급합니다. 따라서 file:// 값이 전달되면, 그 URL을 보낸 WebContent process에까지 해당 디렉터리에 대한 read access가 확장됩니다. 이 확장은 UI process와 Network process 양쪽에서 모두 일어납니다. 결과적으로, file:// grant를 가져본 적이 없는 WebContent process가 임의의 경로를 지정해 그 권한을 얻을 수 있게 됩니다.

Exploit이 성립하려면 renderer가 이미 compromise된 상태여야 합니다. 정상적으로 동작하는 WebContent process는 조작된 ResourceError를 만들어낼 방법이 없기 때문입니다. 이는 sandbox escape의 일반적인 두 번째 단계에 해당하는 위치이며, 이 지점에서 이어지는 확장 primitive로는 매우 직접적인 축에 속합니다. Timing, grooming, memory-layout 제어가 전혀 필요하지 않으며, 이 sequence는 항상 동일하게 재현됩니다.

이 vulnerability는 WebContent-to-UI 권한 경계를, 그 핵심 검증 지점 중 한 곳에서 약화시킵니다. Process에 부여된 URL 집합은 더 이상 renderer가 지정할 수 있는 대상의 기준으로 기능하지 못합니다. renderer가 경로를 지정하면, UI process가 그 경로를 되돌아오는 과정에서 그대로 승인해버리기 때문입니다.