[4] LoadImageForDecoding accepted arbitrary schemes and spoofed first parties
One field of the request was checked. The ones carrying authority were not.
High. Renderer를 대신해 bytes를 가져오는 network-process handler가, URL과 first party, cookie attribution을 함께 담고 있는 struct에서 단 하나의 필드에 대해 단 하나의 속성만 검증하고 있었습니다. 최초 진입점이 되는 bug는 아니며 renderer compromise가 선행되어야 하지만, 그 이후에는 추가 조건 없이 sandbox 경계를 곧바로 넘나드는 read가 가능해집니다.
WebKit은 작업을 여러 process에 분산시킵니다. WebContent process는 엄격한 sandbox 안에서 신뢰할 수 없는 콘텐츠를 렌더링하고, NetworkProcess는 이와 다른 sandbox profile 하에서 네트워크 및 캐시 I/O를 수행합니다. WebContent는 IPC message를 보내는 방식으로 NetworkProcess에 작업을 요청하는데, NetworkConnectionToWebProcess는 network 쪽에서 renderer별로 존재하는 message receiver로서 resource-loading 요청을 처리하는 역할을 맡습니다. 이 요청은 WebCore::ResourceRequest 형태로 전달됩니다. URL뿐 아니라 firstPartyForCookies, isTopSite, sameSiteDisposition, allowCookies, 그리고 임의의 header 필드까지 담고 있는 복합 serialized struct이며, receiver는 이 모든 필드를 attacker-controlled로 취급해야 합니다.
관전 포인트: compromise된 renderer는 이 handler를 file:///private/etc/hosts로 향하게 하거나, 조작된 first party 하의 cross-origin HTTPS URL로 향하게 하여 가져온 bytes를 자신의 address space로 되돌려받을 수 있습니다.
Commit message는 두 가지 공격 경로를 모두 명확히 밝히고 있습니다.
LoadImageForDecoding이url.isValid()검사 하나만으로 임의의ResourceRequest필드를 받아들이고 있었습니다. 이로 인해 NetworkProcess-sandbox 내 파일에 대한file://read와, 조작된firstPartyForCookies를 통한 credentialed cross-origin body read가 가능했습니다. URL을 HTTP(S)로 제한하고allowsFirstPartyForCookies를 강제하여, cookie를 다루는 다른 모든 IPC entry point와 동일하게 맞춥니다.
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
Source/WebKit/UIProcess/WebPageProxy.cpp
LayoutTests/ipc/load-image-for-decoding-file-url.html
Patch Details
클라이언트를 대신해 이미지를 가져오는 두 진입점의 검증이 강화되었습니다. NetworkConnectionToWebProcess::loadImageForDecoding()은 기존에 IPC로 전달된 ResourceRequest를 MESSAGE_CHECK_COMPLETION(url.isValid(), ...) 하나로만 검사했습니다. 이 패치는 해당 검사를 url.isValid() && url.protocolIsInHTTPFamily()로 확장하고, m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow를 요구하는 두 번째 MESSAGE_CHECK_COMPLETION을 추가합니다. 두 검사 중 하나라도 실패하면 completion handler가 빈 ResourceError와 함께 호출되고, 이어서 해당 connection에 대한 message-check 경로가 작동합니다.
WebPageProxy::loadAndDecodeImage()의 early-out에도 동일한 scheme 제한이 추가됩니다. isClosed() || !request.url().isValid() || !request.url().protocolIsInHTTPFamily() 형태입니다. 함께 포함된 나머지 변경분은 새로운 IPCTestingAPI layout test로, m_url과 m_firstPartyForCookies를 file:///private/etc/hosts로 설정한 LoadImageForDecoding 메시지를 직접 손으로 serialize하여 Networking connection에 전송하고, 그 결과로 나오는 invalid-message 문자열에 protocolIsInHTTPFamily가 포함되는지를 검증합니다.
공격자가 serialize한 복합 request struct에 대한 부분적 검증 — 필드 하나의 well-formedness만 확인하고, 권한을 담고 있는 나머지 필드들은 보낸 그대로 신뢰하는 패턴.
Background
Where this lives. WebKit은 작업을 여러 프로세스로 나눕니다. WebContent process는 신뢰할 수 없는 웹 콘텐츠를 좁은 sandbox 안에서 렌더링하고, NetworkProcess는 별도의 sandbox 프로파일 하에서 네트워크와 캐시 I/O를 담당합니다. renderer가 compromise될 수 있다는 것이 전제된 threat이기 때문에, NetworkProcess는 들어오는 메시지의 모든 필드를 공격자가 제어할 수 있는 것으로 취급합니다.
MESSAGE_CHECK_COMPLETION.
IPC handler에서 사용되는 WebKit 매크로입니다. 검증하는 조건이 거짓이면 전달된 completion expression을 실행하여(async reply가 유실되지 않도록) connection에 invalid message를 보고하고, 이는 잘못 동작하는 sender를 종료시킵니다. IPCTestingAPI는 이때 생성되는 invalid-message 문자열을 TakeInvalidMessageStringForTesting을 통해 테스트 코드에 노출합니다.
WebCore::ResourceRequest.
URL과 함께 다수의 로딩 속성을 담는 복합 serializable 타입입니다. firstPartyForCookies, httpMethod, header field들, cache policy, sameSiteDisposition, isTopSite, allowCookies, priority, requester 등이 포함되며, 이 값들은 모두 RequestData variant의 일부로 IPC 경계를 넘습니다.
URL::isValid() vs URL::protocolIsInHTTPFamily().
isValid()는 문자열이 구조적으로 well-formed한 URL로 파싱되었는지를 나타내고, protocolIsInHTTPFamily()는 scheme이 http 또는 https인지를 나타냅니다. 두 속성은 서로 독립적입니다.
firstPartyForCookies and its guard.
firstPartyForCookies는 cookie 로직이 특정 로드의 top-level site로 취급하는 URL로, SameSite 정책 적용과 cookie partitioning 결정을 좌우합니다. NetworkProcess::allowsFirstPartyForCookies(processIdentifier, url)는 특정 web process가 특정 first party를 주장할 권한이 있는지를 확인하는 NetworkProcess 측 registry 검사이며, AllowCookieAccess enum을 반환합니다. 이 값이 Allow인 경우만 통과 상태로 인정됩니다. cookie를 다루는 IPC 진입점에서 표준적으로 사용되는 guard입니다.
Reply shape.
Handler는 Expected<Ref<WebCore::FragmentedSharedBuffer>, WebCore::ResourceError>로 완료됩니다. 즉 가져온 바이트 자체가 caller에게 그대로 돌아갑니다.
IPCTestingAPI.
테스트 전용 기능으로, IPCTestingAPIEnabled test-runner flag 뒤에 gate되어 있으며, layout test가 이름이 지정된 process connection 위에서 raw IPC 메시지를 직접 손으로 serialize하여 전송할 수 있게 해줍니다. 여기서는 compromise된 renderer를 흉내내는 데 사용됩니다.
Analysis
이번 사례는 privilege boundary에서 input validation이 누락된 경우이며, 두 가지 뚜렷한 결과로 이어집니다. 하나는 confused-deputy 방식의 local file read(SSRF 형태의 sandbox 간 정보 노출)이고, 다른 하나는 cookie-attribution spoof(first-party 정책 우회로 인한 credential 포함 cross-origin body 노출)입니다.
Pre-fix Post-fix
WebContent (compromised) WebContent (compromised)
│ LoadImageForDecoding │ LoadImageForDecoding
▼ ▼
┌─ NetworkProcess ───────────┐ ┌─ NetworkProcess ───────────┐
│ url.isValid() ✓ │ │ isValid() && HTTPFamily ✗ │
│ scheme: (unchecked)│ │ allowsFirstParty… ✗ │
│ firstParty: (unchecked)│ │ └─► invalid message, │
│ └─► fetch file:// ────┼──┐ │ connection killed │
└────────────────────────────┘ │ └────────────────────────────┘
FragmentedSharedBuffer ◄───┘
(bytes returned to sender)
Handler는 공격자가 완전히 serialize할 수 있는 ResourceRequest를 그대로 받아, 그중 정확히 하나의 필드에 대한 하나의 속성만 검증했습니다. 바로 url.isValid()인데, 이는 URL이 well-formed하게 파싱되었는지만 확인할 뿐 scheme에 대해서는 아무것도 말해주지 않습니다. 누락된 두 가지 invariant는 NetworkProcess IPC 표면의 다른 request-bearing 메시지들이 이미 강제하고 있는 것들입니다. 하나는 request URL이 HTTP family에 속해야 한다는 것으로, 이는 낮은 권한의 process가 NetworkProcess로 하여금 자기 자신의 resource namespace를 향해 다른 scheme으로 읽기를 수행하도록 지시할 수 없게 합니다. 다른 하나는 firstPartyForCookies가 요청을 보낸 web process가 등록상 주장할 수 있는 값이어야 한다는 것입니다.
Handler는 request를 network session의 loader로 그대로 전달하고, 가져온 바이트를 Expected<Ref<FragmentedSharedBuffer>, ResourceError> 형태로 caller에게 반환합니다. 즉 raw response body가 IPC를 통해 요청한 process에게 그대로 넘어가는 것이지, 단순히 requester가 이미 통제하고 있는 어딘가에 decoding된 pixel만 렌더링되는 구조가 아닙니다. 바로 이 reply shape 때문에 두 검증 누락이 각각 disclosure primitive로 작동하게 됩니다. Scheme 축에서 보면, URL scheme에 제약이 없었으므로 file:// URL이 isValid()를 통과했고, 그 fetch는 WebContent와는 다른 sandbox 프로파일을 가진 NetworkProcess에 의해 수행되었습니다. Cookie 축에서 보면, allowsFirstPartyForCookies 검사가 없는 상태에서 sender는 firstPartyForCookies를, 그리고 같은 serialize된 struct 안의 m_isTopSite와 m_sameSiteDisposition을 피해자 origin으로 설정하면서 동시에 cross-origin URL을 요청할 수 있었습니다. 그 결과 로드는 피해자의 cookie가 첨부된 상태로 발생하고, response body는 sender에게 그대로 반환됩니다. 이 commit의 test case는 정확히 이 구조를 재현합니다. RequestData variant를 손으로 구성하면서 m_url과 m_firstPartyForCookies를 모두 file:///private/etc/hosts로, m_allowCookies: true, m_isTopSite: true로 설정합니다.
전제 조건은 이미 compromise되었거나(또는 IPC를 직접 다룰 수 있는) WebContent process입니다. loadImageForDecoding은 web에 노출된 API가 아니라 NetworkProcess IPC handler이므로, 사전에 renderer가 compromise되었거나 test 전용 IPCTestingAPI를 사용하지 않는 한 일반적인 JavaScript로는 도달할 수 없습니다. 이 전제가 성립한다는 가정 하에, 이번 commit의 test가 곧 trigger 절차 그 자체입니다. 목표 URL을 m_url에 넣은 RequestData variant를 구성하고, m_firstPartyForCookies와 m_allowCookies: true, m_isTopSite: true를 원하는 값으로 설정한 뒤, NetworkConnectionToWebProcess_LoadImageForDecoding 메시지 정의와 함께 자기 자신의 pageID, 그리고 공격자가 임의로 지정한 maximumBytesFromNetwork를 넣어 serialize하고, Networking connection 위로 전송합니다.
여기서 두 방향의 시나리오가 이어집니다. Scheme 쪽 경로에서는, m_url을 file:// 경로로 설정하면 패치 이전의 isValid()만 확인하는 guard를 통과하게 되고, 그 로드는 renderer가 아니라 NetworkProcess의 session에 의해 처리됩니다. Completion handler가 가져온 바이트를 FragmentedSharedBuffer로 sender에게 반환하므로, 파일 내용은 compromise된 renderer가 읽을 수 있는 상태가 되며, 그 크기는 sender가 함께 제어하는 maximumBytesFromNetwork로 제한됩니다. Commit 메시지는 이 경로가 실제로 NetworkProcess sandbox 내 파일에 도달했다고 서술하고 있습니다. 다만 제공된 context는 MESSAGE_CHECK 지점까지만 다루고 있고, file://이 실제로 처리되는 모습을 보여줄 NetworkLoad/session 코드 경로는 포함하지 않습니다. 따라서 이 부분은 저자의 주장을 그대로 옮긴 것입니다. Cookie 쪽 경로에서는, m_firstPartyForCookies를 피해자 사이트로 설정한 채 cross-origin HTTPS URL을 요청하면 해당 로드가 SameSite와 partitioning 판단에서 first-party로 취급됩니다. 그 결과 credential이 포함된 response를 가져올 수 있고, 그 body는 동일한 completion handler를 통해 반환될 수 있습니다. 이 경로가 실제로 성립하려면 목표 endpoint가 cookie 정책이 원래 차단했을 콘텐츠를 서빙해야 하고, loader와 completion handler 사이에 별도의 CORS/response-filtering 단계가 없어야 한다는 추가 조건이 필요합니다. 제공된 context에는 load path의 그 부분이 포함되어 있지 않습니다.
이 취약점은 WebContent-to-NetworkProcess 사이의 privilege boundary와, 별개로 cookie/same-site 정책 boundary를 함께 약화시킵니다. 보안 모델은 renderer로부터 오는 request-bearing IPC에 대해 두 가지를 전제합니다. 하나는 renderer가 NetworkProcess를 deputy로 이용해 자신의 sandbox 밖 resource namespace에 접근할 수 없어야 한다는 것이고, 다른 하나는 renderer가 cookie-attachment 판단에서 임의의 first-party identity를 주장할 수 없어야 한다는 것입니다. 후자는 정확히 allowsFirstPartyForCookies()가 강제하기 위해 존재하는 규칙입니다. 패치 이전 상태에서는, 이미 WebContent process를 compromise한 공격자가 이 메시지를 NetworkProcess sandbox에서 접근 가능한 파일에 대한 read primitive로 사용할 수 있었고, spoof된 first party 하에서 credential이 포함된 cross-origin 로드를 발생시켜 그 response body를 IPC를 통해 돌려받을 수 있었습니다. 여기서 얻어지는 것은 local file 내용과 cross-origin 인증된 response body라는 정보 노출이며, memory corruption이 아니라 privilege escalation과 cross-site data theft로 이어질 수 있는 chain 소재에 해당합니다.
Insight
이 commit 메시지 자체의 표현 — "matching every other cookie-touching IPC entry point" — 이 흥미로운 부분입니다. allowsFirstPartyForCookies는 이미 확립되어 널리 적용되고 있는 guard였고, 이 handler는 단지 그 guard가 빠져 있던 하나의 예외였을 뿐입니다. 이는 관례가 확립된 이후에 추가된 진입점이 그 관례를 따르지 않은 전형적인 패턴이며, 따라서 감사 단위로 생산적인 접근은 "bounds check 누락을 찾는다"가 아니라 "ResourceRequest를 받는 모든 IPC handler를 나열하고 각각의 guard prologue를 관례와 비교한다"는 방향이 되어야 함을 시사합니다. 아울러 이번 fix는 firstPartyForCookies와 scheme만 제한한다는 점도 짚어둘 만합니다. 같은 serialize된 RequestData에는 여전히 m_isTopSite, m_sameSiteDisposition, m_allowCookies, 그리고 renderer가 임의로 지정하는 m_httpHeaderFields가 함께 실려 있고, test에서도 m_isTopSite: true를 명시적으로 설정하고 있습니다. renderer가 전달한 이 나머지 신뢰 신호들이 NetworkProcess 측에서 독립적으로 다시 검증되는지는 이 패치가 다루지 않는 별개의 질문입니다.
Audit directions
- Partial validation of a compound cross-privilege struct. 더 높은 권한의 process가 더 낮은 권한의 process로부터 serialize된 struct를 받으면서, 필드 하나의 well-formedness만 검증하고 권한을 담고 있는 필드들은 그대로 신뢰하는 패턴입니다. 여기서 지켜야 할 invariant는 authorization이나 namespace 판단에 사용되는 cross-privilege struct의 모든 필드는, 검사하기 가장 쉬운 필드 하나만이 아니라 각각 독립적으로 재검증되어야 한다는 것입니다. 좁게 보면,
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp와NetworkProcess.cpp에서WebCore::ResourceRequest&&또는NetworkResourceLoadParameters&&를 인자로 받는 handler들을 나열하고, 각각의MESSAGE_CHECK/MESSAGE_CHECK_COMPLETIONprologue를 이번 commit이 도입한 검증 쌍과 비교해보아야 합니다. 매칭 신호는 다중 필드 struct의 한 member에 대한isValid()형태의 단일 predicate만 guard로 존재하는 handler입니다. 넓게 보면, 동일한 패턴은 WebKit의 어떤 process 경계에서든 rich type을 deserialize하면서 member 하나만 검사할 때 나타날 수 있습니다. GPUProcess와 UIProcess의 message 표면에서ClientOrigin,SecurityOriginData,WebCore::ResourceResponse인자에도 같은 관점을 적용할 수 있으며, 코드 검색 결과에서 struct member 수가 guard prologue의 predicate 수를 크게 초과하는 handler body가 매칭 신호입니다. 가장 넓게 보면, 이는 "trust boundary에서 이루어지는 복합 메시지의 부분적 검증"이라는 일반적인 클래스이며, receiver에게 scalar가 아니라 struct를 그대로 넘기는 어떤 IPC/RPC 시스템에서도 나타날 수 있습니다. Chromium의 MojoStructTraits::Read, Android Binder의Parcelreader, 사용자 공급 struct에 대한 kernel의copy_from_user모두 같은 성질을 보입니다. 다른 codebase에도 적용할 수 있는 핵심 질문은 "receiver가 이 필드로부터 어떤 판단을 내리는가, 그리고 만약 그렇다면 이 필드를 설정할 권한은 누구에게 있는가"입니다.
빅테크 보안 뉴스레터 섹션 번역 작업을 진행합니다.
-
URL-scheme confused deputy 패턴. 상위 권한을 가진 fetcher가 하위 권한 caller로부터 URL을 받아, 이를 상위 권한 프로세스 자신의 resource namespace 안에서 resolve하는 상황입니다. 여기서 지켜야 할 invariant는, 권한 경계를 넘어 안쪽으로 들어오는 URL은 caller가 원래 스스로 접근할 수 있었던 scheme으로만 제한되어야 한다는 것입니다. Narrow하게 보면, NetworkProcess의 IPC handler들에서
url.isValid()가 호출되는 지점 중protocolIsInHTTPFamily()나 이에 준하는 scheme allowlist가 동반되지 않은 곳을 검색해야 합니다. 특히 preconnect, ping, speculative-load 진입점을 우선 점검할 필요가 있습니다. 이때 매치 tell은 parseability만 제약하고 scheme은 제약하지 않는 predicate를 가진MESSAGE_CHECK입니다. Wider하게 보면, 같은 클래스는 caller가 넘긴 locator를 dereference하는 모든 상위 권한 컴포넌트에 적용됩니다. custom URL scheme handler 등록, 프로세스 경계를 넘는 blob 및data:URL resolution, file-backedSandboxExtension소비 등이 여기에 해당합니다. 매치 tell은 locator가 앞선 allowlist 검사 없이 resolver로 그대로 전달되는 호출 지점입니다. Widest하게 보면, 이는 HTTP를 넘어 일반화된 전형적인 SSRF/confused-deputy 문제입니다.curl기반 webhook, image proxy, PDF renderer 같은 server-side URL fetcher는 물론, client가 지정한 경로를 여는 모든 local 상위 권한 daemon에도 동일하게 적용됩니다. 코드베이스 전반에 걸쳐 유지되어야 할 invariant는, caller보다 넓은 reach를 가진 컴포넌트가 caller가 제공한 이름을 dereference할 때는 scheme/namespace가 경계 지점에서 반드시 allowlist 방식으로 검증되어야 하며, 다운스트림에서 denylist 방식으로 처리되어서는 안 된다는 점입니다. -
Authorization decision에 사용되는 client-asserted trust label.
firstPartyForCookies는 이제 이 지점에서 검증되지만, 같은 serializedRequestData안에는 여전히 renderer로부터 전달된m_isTopSite,m_sameSiteDisposition,m_allowCookies,m_httpHeaderFields가 함께 실려 있습니다. 실제로 이번 commit 자체의 test 코드에서도m_isTopSite: true를 그대로 설정하고 있습니다. Narrow하게 보면,NetworkProcess::allowsFirstPartyForCookies를 사용하는 모든 지점을 추적하여, 같은 request 상의 부가 attribution field들이 재계산되는지 아니면 전달된 값을 그대로 신뢰하는지 각각 확인해야 합니다. 매치 tell은 IPC로 도착한 request에 대해request.isTopSite()나request.sameSiteDisposition()을 읽어 cookie 또는 partitioning을 결정하는 코드입니다. Wider하게 보면, NetworkProcess와 storage layer로 넘어오는 다른 renderer-supplied identity signal들도 함께 점검할 필요가 있습니다.ClientOrigin,WebPageProxyIdentifier,SecurityOriginData인자가 여기에 해당하며, 매치 tell은 process identifier와 origin과 유사한 값을 모두 인자로 받으면서도 둘 중 하나만 실제로 참조하는 함수입니다. Widest하게 보면, 이는 "caller가 자신의 identity claim을 스스로 제공한다"는 일반적인 클래스에 해당하며, request가 channel에 bound된 identity와 self-asserted identity를 동시에 담고 있는 모든 상황에서 성립합니다. 검증되지 않은 JWT claim, application tier에서 신뢰되는X-Forwarded-For, request body에서 읽어오는 tenant ID 등이 대표적인 예입니다. 코드베이스 전반에 걸쳐 유지되어야 할 invariant는, authorization decision에 사용되는 identity는 반드시 request가 도착한 channel에 바인딩되어야 하며, request payload로부터 읽어와서는 안 된다는 점입니다. -
비동기 handler에 새로 추가된
MESSAGE_CHECK_COMPLETIONguard의 completion-path 동작. Request를 실패 처리함과 동시에 connection을 종료시키는 validation macro라 하더라도, async-reply contract는 그대로 만족시켜야 합니다. 그렇지 않으면 caller 측 completion handler가 leak되거나 reply 자체가 누락됩니다. Narrow하게 보면,Source/WebKit/NetworkProcess전반에서 마지막 parameter로CompletionHandler를 받는 함수들 안의MESSAGE_CHECK_COMPLETION을 검색하여, early-exit 경로마다 completion expression이 유효한 실패 값을 구성하는지 확인해야 합니다. 매치 tell은 completion 인자의 형태가 해당 handler의 success-path completion 호출과 다른MESSAGE_CHECK_COMPLETION입니다. Wider하게 보면, 같은 형태는 GPUProcess의 remote-object surface를 포함해 여러 개의 guard-then-return 지점을 가진 모든 비동기 handler에서 나타날 수 있습니다. 매치 tell은 distinct completion-handler 호출 수보다return문의 수가 더 많은 함수입니다. 이 클래스는 validation 실패와 connection teardown을 하나로 결합한 WebKit 고유의MESSAGE_CHECK_COMPLETION_BASEidiom에 종속됩니다. Chromium의 Mojo는ReportBadMessage와 callback resolution을 분리해서 다루기 때문에 정확히 같은 형태로는 이전되지 않으며, 이 audit의 범위는 WebKit의 IPC 경계 안으로 한정됩니다.