[2] Digital Credentials chooser trusted renderer-supplied origins
A compromised renderer could put any site's name on the system credential chooser.
Medium이며, renderer가 이미 장악된 이후에만 의미가 있습니다. 메모리 손상은 발생하지 않습니다. 다만 compromised WebContent process가 피해 사이트의 origin을 신뢰된 시스템 동의 UI에 표시하도록 만들 가능성이 있습니다. 이것이 실제 credential misbinding으로 이어지는지는 플랫폼 chooser가 전달받은 origin을 어떻게 사용하는지에 달려 있습니다.
WebKit의 multi-process 모델에서 중개되는 동의 UI는 요청 주체를 privileged UI process가 직접 판단해야 안전합니다. sandbox된 renderer의 주장을 그대로 믿어서는 안 됩니다. Digital Credentials API의 요청은 WebPageProxy로 들어옵니다. WebPageProxy는 페이지의 IPC message를 수신하는 UI process 측 객체입니다. 이 API는 플랫폼 wallet에 모바일 운전면허증 같은 신분 문서를 제시하도록 요청합니다. 이때 각 요청에는 frame identifier와 함께 top-level origin, 요청 document의 origin이 포함됩니다. 시스템 credential chooser에 표시되는 신원은 원래 message를 보낸 connection에 묶인 UI process state에서 가져와야 합니다.
관전 포인트: WebContent process를 이미 장악한 attacker라면, 위조한 요청 origin으로 시스템 credential chooser를 띄우는 것이 이론적으로 가능합니다. 이 경우 chooser에는 신뢰할 수 있는 relying party가 표시됩니다. 결과적으로 사용자가 이를 믿고 신원 credential을 넘기도록 유도될 가능성이 있습니다.
Digital Credentials: 신뢰된 UI process state에서 요청 origin 도출. 기존 WebPageProxy::showDigitalCredentialsChooser는 requestData에 담긴 WebContent 제공 origin을 그대로 사용해 credential chooser를 표시했습니다. 문제는 이 값을 compromised WebContent process가 임의로 조작할 수 있다는 점입니다. 수정 후에는 IPC frame identifier로 요청 frame을 조회하고, 해당 frame이 이 페이지에 없으면 요청을 거부합니다. 또한 IPC가 그 frame의 process에서 왔는지 message check로 검증합니다. 이어서 UI process의 신뢰된 frame state에서 top-level origin과 요청 origin을 도출합니다. 이 값으로 requestData를 정제한 뒤에야 chooser를 표시합니다. 원래 316606.155@safari-7625-branch로 반영된 변경입니다.
Source/WebKit/UIProcess/WebPageProxy.cpp
LayoutTests/ipc/digital-credentials-chooser-frame-check.html
Patch Details
WebPageProxy::showDigitalCredentialsChooser의 HAVE(DIGITAL_CREDENTIALS_UI) 분기 안에서 세 가지가 변경되었습니다. 먼저 WebFrameProxy::webFrame(frameID)로 요청 frame을 조회하고 this->mainFrame()을 가져오도록 수정되었습니다. 둘 중 하나라도 null이거나 requestingFrame->page() != this이면 AbortError로 요청을 거부합니다. 다음으로 기존 MESSAGE_CHECK_COMPLETION_BASE 조건이 변경되었습니다. 기존 check는 renderer가 보낸 requestData.topOrigin을 SecurityOrigin::create(mainFrame()->url())과 isSameOriginDomain으로 비교했습니다. 새 조건은 &requestingFrame->process() == WebProcessProxy::fromConnection(connection).ptr()을 요구합니다. 실패 메시지도 일반적인 "Digital credentials request is not allowed."로 바뀌었습니다. 마지막으로 user activation이 사용된 이후 sanitizedRequestData를 생성합니다. 이 복사본의 topOrigin과 documentOrigin은 main frame과 요청 frame의 WebFrameProxy에서 얻은 documentSecurityOriginData()로 덮어쓰입니다. 이후 원본 데이터 대신 이 복사본이 PageClient::showDigitalCredentialsChooser로 전달됩니다. 한편 null frameID로 위조 message를 보내는 IPC layout test가 새로 추가되었고, glib TestExpectations 항목에서는 이 test를 skip합니다.
Privilege boundary에서의 confused deputy: broker가 인증된 호출자에 묶인 broker 측 state가 아니라, 비권한 client가 스스로 보고한 principal을 기준으로 동의 결정을 내리는 패턴.
Background
이 코드가 있는 위치.
WebKit은 web content를 sandbox된 WebContent process에서 실행합니다. GPU process와 Networking process는 그래픽과 네트워크 I/O를 담당합니다. UI process는 WebKit을 내장한 앱 자체로, frame tree, commit된 origin, user activation timestamp 같은 신뢰된 state를 보관합니다. WebContent는 IPC로 UI process와 통신하며, WebPageProxy 같은 클래스가 이 message를 수신합니다.
IPC 검증 매크로.
MESSAGE_CHECK와 그 변형인 MESSAGE_CHECK_COMPLETION_BASE는 compromised renderer에 대한 WebKit의 표준 방어 수단입니다. 조건이 실패하면 해당 message는 비정상 process가 보낸 것으로 간주됩니다. 이때 message는 invalid로 표시되고, 대개 해당 WebContent process가 종료됩니다. 전달된 completion도 함께 실행됩니다.
WebFrameProxy와 site isolation.
WebFrameProxy는 UI process가 frame을 표현하는 객체입니다. WebFrameProxy::webFrame(frameID)는 UI process의 map에서 frame을 조회합니다. page()는 소유 WebPageProxy를, process()는 해당 frame을 호스팅하는 WebProcessProxy를 반환합니다. Site isolation 환경에서는 한 페이지의 frame들이 서로 다른 WebContent process에 있을 수 있습니다. 즉 페이지를 안다고 해서 어떤 process가 message를 보냈는지까지 알 수는 없습니다.
Commit된 origin과 URL 기반 origin.
documentSecurityOriginData()는 frame에 commit된 document의 origin입니다. 반면 SecurityOrigin::create(url)은 URL 문자열에서 origin을 계산합니다. about:blank나 sandbox된 frame처럼 origin을 상속받거나 opaque origin을 갖는 document에서는 두 값이 달라질 수 있습니다.
Digital Credentials.
navigator.credentials.get에 digital 멤버를 지정하면, 사이트가 플랫폼 wallet에 신분 문서를 요청할 수 있습니다. ISO mdoc 형식의 모바일 운전면허증이 대표적인 예입니다. 요청 데이터(DigitalCredentialsMobileDocumentRequestData)에는 topOrigin과 documentOrigin이 들어 있습니다. 브라우저는 시스템 chooser를 띄우고, 사용자는 여기서 해당 요청자에게 credential을 공유할지 동의합니다.
Analysis
UI process는 serialize된 요청 데이터를 그대로 믿고 요청 주체를 판단했습니다. 즉 IPC validation이 누락된 logic error에 해당합니다.
WebContent (compromised) UI process (WebPageProxy)
┌───────────────────────┐ ┌──────────────────────────────────────┐
│ ShowDigitalCredentials│ IPC │ Before: topOrigin ~ create(mainURL)? │
│ Chooser(frameID, │ ───────► │ documentOrigin: unchecked │
│ topOrigin, │ │ frameID: not bound to sender │
│ documentOrigin) │ │ requestData ──► system chooser │
└───────────────────────┘ │ After: frame in page? sender hosts │
│ it? origins overwritten from │
│ WebFrameProxy ──► system chooser │
└──────────────────────────────────────┘
다이어그램의 "Before" 상자에는 세 가지 빈틈이 있습니다. (a) 실제로 요청을 보내는 frame의 origin인 documentOrigin은 전혀 검증되지 않았고, 그대로 chooser에 전달되었습니다. (b) HAVE(DIGITAL_CREDENTIALS_UI) 분기 안에서는 IPC frameID를 조회하지도, 송신 connection에 묶지도 않았습니다. null frameID, 다른 페이지에 속한 frame, 다른 WebContent process가 호스팅하는 frame이 모두 여기에 해당합니다. 마지막 경우는 여러 process가 하나의 WebPageProxy와 통신하는 site isolation 환경에서 특히 중요합니다. 다만 showDigitalCredentialsChooser의 앞부분에서 frameID를 제약했는지는 아직 열린 질문입니다. 아무 제약도 없었다면 세 경우 모두 chooser까지 도달했을 것입니다. (c) topOrigin check조차 기대값을 main frame의 commit된 document origin이 아니라 URL에서 만들었습니다. 그래서 URL이 없거나 origin을 상속받는 document에서는 두 값이 어긋날 수 있었습니다.
Regression test는 IPC testing API를 통해 compromised renderer threat model을 직접 재현합니다.
IPC.connectionForProcessTarget("UI")를 획득합니다.WebPageProxy_ShowDigitalCredentialsChooser인자를 serialize합니다. 이때frameID: {}(null)와 함께, origin과 빈requests를 test가 임의로 정한DigitalCredentialsMobileDocumentRequestData를 사용합니다.- message를 전송하고, 거부 메시지 "Digital credentials request has no active frame."을 assert합니다.
이 test는 패치 이후의 거부 동작만 확인합니다. 기존 분기는 frame 조회를 전혀 수행하지 않았습니다. 따라서 showDigitalCredentialsChooser의 앞부분에서 거부하지 않는 한, null frameID가 chooser까지 도달했을 것입니다.
일반 web content로는 이 경로에 도달할 수 없습니다. Renderer 측 coordinator는 두 origin을 정직하게 채우기 때문입니다. message를 위조하려면 IPCTestingAPIEnabled가 켜져 있거나 WebContent process가 compromise된 상태여야 합니다. 이런 compromise를 전제하면, 패치 이전 handler는 위조된 origin과 sender에 묶이지 않은 frame identifier를 받아들였을 것입니다. 그리고 이를 그대로 플랫폼 chooser로 전달했을 것입니다. 메모리 손상이 아니라, 중개된 capability를 2단계로 악용하는 형태입니다. 피해 범위는 사용자 동의와 lastActivationTimestamp 사용 여부로 제한되며, 이 timestamp는 여전히 매 요청을 gate합니다.
확장 여부를 가르는 질문은 PageClient::showDigitalCredentialsChooser 뒤의 chooser가 topOrigin과 documentOrigin을 어떻게 사용하는지입니다. 단순한 표시용 문자열이라면, 영향의 상한은 사용자 기만에 의존하는 UI spoofing에 그칠 것으로 보입니다. 반대로 origin 범위의 session transcript나 암호화 context처럼, origin에 묶인 응답 생성에 쓰인다면 상황이 달라집니다. 이 경우 compromised renderer가 위조된 origin을 이용해, 피해 relying party 범위로 한정된 credential presentation을 얻을 가능성이 있습니다.
이 vulnerability는 WebContent process와 UI process 사이의 isolation 경계를 약화시킵니다. 깨진 invariant는 다음과 같습니다. 신뢰된 시스템 UI에 표시되거나 그 안에 결합되는 신원 주장은, 송신 connection에 묶인 UI process state에서 나와야 합니다. Renderer가 serialize한 데이터에서 나와서는 안 됩니다. 패치 이전에는 compromised renderer가 위조된 요청 origin을 chooser에 표시하고, 그 값으로 동작하게 만들 수 있었습니다. URL 기반 check는 통과하지만 commit된 document와는 일치하지 않는 topOrigin도 마찬가지였습니다.
Insight: 패치 이전 코드에도 MESSAGE_CHECK는 있었습니다. 문제는 검증 대상과 방식이 모두 잘못되었다는 점입니다. renderer가 보낸 두 origin 중 하나만 검사했고, 비교 기준도 URL에서 만든 origin이었습니다. 송신 process와의 결합도 없었습니다. Renderer 데이터를 broker state와 비교하는 validator는, renderer 데이터를 버리고 broker state로 치환하는 방식보다 약합니다. 이번 fix가 택한 것이 바로 후자입니다. requestingFrame->process() == WebProcessProxy::fromConnection(connection) 비교는 이 방어를 site isolation 시대에 맞게 다듬은 형태입니다. 여러 WebContent process가 하나의 WebPageProxy를 공유하게 되면, 페이지를 신뢰하는 것만으로는 충분하지 않습니다. IPC로 전달되는 모든 frameID를 송신자에 묶어야 합니다.
Audit directions
-
Broker가 self-reported principal을 신뢰하는 패턴. 비권한 client가 보고한 origin, URL, package, 앱 identity를 그대로 신뢰하는 privileged broker는 confused deputy에 해당합니다. Narrow:
Source/WebKit/UIProcess에서 IPC handler를 검색합니다. 대상은 인자에SecurityOriginData,URL, 또는 origin 필드를 가진 요청 구조체를 포함하고, 그 값으로 권한 prompt나 시스템 UI를 구동하는 handler입니다. WebAuthn proxy, share sheet, contact picker, 미디어 및 알림 권한 handler부터 살펴보는 것이 좋습니다. 핵심 신호는WebFrameProxy::documentSecurityOriginData()나 그에 준하는 UI process 조회 대신 message에서 origin을 읽는 handler입니다. Wider: broker가 자체 기록보다 renderer가 보고한 frame URL, top frame URL, user activation flag를 신뢰하는 경우에도 같은 형태가 나타납니다. Widest: 결국 "client가 스스로 밝힌 신원을 믿지 말라"는 일반 원칙입니다. Chromium은 이를 "renderer가 보낸 origin이 아니라RenderFrameHost::GetLastCommittedOrigin을 사용하라"는 규칙으로 구현합니다. Android Binder 서비스는 호출자가 제공한 package 이름 대신getCallingUid()를 사용합니다. XPC 서비스는 self-reported PID 대신 audit token을 사용합니다. 어디서든 신호는 같습니다. 인증된 channel이 아니라 요청 payload에서 principal을 가져와 인가나 동의를 결정하는 지점입니다. -
송신자에 묶이지 않은 frame identifier.
FrameIdentifier를 받아 frame에 대해 동작하는 모든 UIProcess IPC handler를 점검해야 합니다. frame이 이 페이지에 속하는지, 그리고frame->process()가 connection 뒤의 process와 같은지를 모두 확인하는지 살펴볼 필요가 있습니다. Narrow:WebPageProxy.cpp와 다른*Proxy.cppmessage receiver에서WebFrameProxy::webFrame(을 검색합니다. 이후process() == WebProcessProxy::fromConnection(connection)비교 없이 frame을 사용하는 지점을 찾으면 됩니다. 이 비교가 빠져 있다는 것이 핵심 신호입니다. Wider: multi-tenant broker에서 identifier를 key로 조회하는 모든 지점이 같은 형태를 보일 수 있습니다. page, frame, worker, resource ID를 global map에서 조회하면서 호출자 기준의 ownership check를 하지 않는 경우가 여기에 포함됩니다. 이 class는 WebKit의 site isolation 모델과 밀접하게 연결되어 있습니다. 외부에서 가장 가까운 사례는 Chromium의RenderProcessHost별 routing ID 검증입니다. 다만MESSAGE_CHECK관용구 자체가 그대로 옮겨지지는 않습니다. -
URL에서 유도한 expected origin. 보안 검사에서 committed document origin을 쓰지 않고
SecurityOrigin::create(url)로 expected origin을 구성하는 지점을 점검할 필요가 있습니다. 좁은 범위에서는Source/WebKit/UIProcess에서SecurityOrigin::create(호출을 검색합니다. 이때MESSAGE_CHECK조건이나 permission 결정 안에서mainFrame()->url()또는 frame의url()에 적용된 경우를 찾으면 됩니다. 검색된 지점마다about:blank,data:, blob, sandboxed document가 origin 불일치를 일으킬 가능성이 있습니다. 넓은 범위로 확장하면, URL을 key로 쓰는 cookie partitioning, storage access, CSP reporting 경로에서도 비슷한 URL-origin 혼동이 나타납니다. 가장 넓은 범위에서 보면, document의 security principal은 URL이 아니라 committed document에 속하는 속성입니다. Chromium이GetLastCommittedURL과GetLastCommittedOrigin을 분리한 것도 같은 원칙을 반영합니다. HTML spec이about:blank와 sandboxed iframe에 origin 상속을 규정한 것 역시 마찬가지입니다. 판별 단서는 한쪽 값은 URL에서 계산되고, 다른 쪽은 document나 policy origin인 비교 구문입니다. -
필드나 variant를 놓치는 overwrite 방식 sanitize. Overwrite 방식의 sanitize fix가 renderer가 전달하는 모든 필드를 다루는지 확인해야 합니다.
Variant의 모든 alternative를 빠짐없이 처리하는지도 함께 살펴볼 필요가 있습니다. 좁은 범위에서 보면,DigitalCredentialsRequestData는 현재 alternative가 하나(DigitalCredentialsMobileDocumentRequestData)뿐인Variant입니다. 그런데sanitizedRequestData는topOrigin과documentOrigin만 덮어씁니다. 따라서 나머지requestspayload에 origin 범위의 데이터가 담기는지 확인해야 합니다. 향후 추가될 alternative에도 같은 수준의 sanitize가 적용되는지 역시 점검 대상입니다. 넓은 범위로 확장하면, client struct를 복사한 뒤 일부 필드만 수정하는 broker는 모두 같은 위험을 안고 있습니다. 나중에 추가되는 필드에 대해서는 검사 없이 통과시키는 fail-open 동작이 되기 때문입니다. 판별 단서는 필드나 variant를 전부 나열하지 않는 copy-then-overwrite 구조입니다. default branch를 둔switchOn이 대표적인 예입니다.