← All issues

[1] LoadImageForDecoding scheme and cookie-access validation

The decode endpoint that checked a URL was valid — and nothing else.

Severity: High | Component: WebKit NetworkProcess IPC | ccf0c48

Severity를 High로 평가한 이유는, 패치 이전 endpoint가 WebContent로부터 attacker-controlled ResourceRequest를 그대로 받아들이면서 url.isValid()만 검증했기 때문입니다. 이로 인해 compromised renderer가 file:// load에 도달하고 임의의 cookie first-party를 NetworkProcess에 주장할 수 있는 경로가 열려 있었습니다. 실제 local-file 유출이나 cross-origin credentialed read로의 확장 여부는 diff에 드러나지 않는 downstream loader 동작에 달려 있습니다. 다만 이번 fix는 바로 그 경로에 필요한 scheme 검증과 cookie 인가 검사를 정확히 추가하고 있습니다.

LoadImageForDecodingurl.isValid() 검사 하나만으로 임의의 ResourceRequest 필드를 받아들이고 있었습니다. 그 결과 NetworkProcess sandbox 내 파일을 file://로 읽어올 수 있었고, 위조된 firstPartyForCookies를 통해 credentialed cross-origin body read도 가능했습니다. 이번 수정으로 URL은 HTTP(S)로 제한되었고, allowsFirstPartyForCookies 검사도 강제되어 cookie를 다루는 다른 모든 IPC entry point와 동일한 기준을 갖추게 되었습니다.

Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp

void NetworkConnectionToWebProcess::loadImageForDecoding(WebCore::ResourceRequest&& request, WebPageProxyIdentifier pageID, uint64_t maximumBytesFromNetwork, ...)
{
auto url = request.url();
- MESSAGE_CHECK_COMPLETION(url.isValid(), completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
+ MESSAGE_CHECK_COMPLETION(url.isValid() && url.protocolIsInHTTPFamily(), completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
+ MESSAGE_CHECK_COMPLETION(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow, completionHandler(makeUnexpected<WebCore::ResourceError>({ })));

Source/WebKit/UIProcess/WebPageProxy.cpp

void WebPageProxy::loadAndDecodeImage(WebCore::ResourceRequest&& request, ...)
{
- if (isClosed() || !request.url().isValid())
+ if (isClosed() || !request.url().isValid() || !request.url().protocolIsInHTTPFamily())
return completionHandler(makeUnexpected(decodeError(request.url())));

LayoutTests/ipc/load-image-for-decoding-file-url.html

+ const fileRequest = { getRequestDataToSerialize: { variant: { m_url: { string: 'file:///private/etc/hosts' }, ... } } };
+ const definition = IPC.messages.NetworkConnectionToWebProcess_LoadImageForDecoding;
+ // asserts: 'Message check failed' && 'protocolIsInHTTPFamily'

NetworkProcess 쪽에서는 NetworkConnectionToWebProcess::loadImageForDecoding의 기존 MESSAGE_CHECK_COMPLETION(url.isValid(), ...)url.protocolIsInHTTPFamily() 조건이 추가되었습니다. 여기에 더해, m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow를 요구하는 두 번째 MESSAGE_CHECK_COMPLETION이 새로 삽입되었습니다. UIProcess 쪽에서는 WebPageProxy::loadAndDecodeImage의 early-return guard에 || !request.url().protocolIsInHTTPFamily()가 추가되었습니다. Regression test는 IPC testing API를 통해 file:///private/etc/hosts URL을 가진 ResourceRequest를 직접 조작하여 전송하고, 이제 protocolIsInHTTPFamily에서 message가 거부되는지 확인합니다.

IPC 경계에서 attacker-controlled ResourceRequest 필드를 신뢰하면서, 전송 측 process에 대한 scheme 및 cookie-access 인가를 강제하지 않은 패턴입니다.

NetworkProcess는 WebContent로부터 loadImageForDecoding과 같은 IPC endpoint를 통해서만 도달 가능합니다. 이 endpoint는 sandbox化된 WebContent process를 대신해 이미지를 가져오고 decode하는 privileged endpoint입니다. ResourceRequest는 URL, firstPartyForCookies, method, header, cookie policy 등 load 관련 파라미터를 담은 완전히 serialize 가능한 묶음이며, WebContent process가 이를 구성해 IPC로 전송합니다. MESSAGE_CHECK와 그 completion 변형은 수신된 message에 대한 assertion을 검증하는 역할을 합니다. 검증에 실패하면 연결을 종료하거나 flag를 남기고 진행하지 않으므로, 이는 권한이 낮은 process로부터 도착한 message에 대한 표준 인가 게이트에 해당합니다. protocolIsInHTTPFamily()는 http/https URL에 대해서만 true를 반환하는 predicate입니다. firstPartyForCookies는 어떤 cookie를 첨부할지 결정할 때 first-party/top-site context로 사용되는 URL이며, allowsFirstPartyForCookies(processID, url)는 특정 web process가 특정 URL을 그 first-party로 사용할 권한이 있는지 확인하는 NetworkProcess의 policy check입니다. Regression test에서 사용된 IPC testing API는 페이지가 raw IPC message를 직접 합성할 수 있게 해주며, 이를 통해 compromised WebContent process 상황을 모델링합니다.

이번 사안은 memory-safety bug가 아니라 IPC input-validation / access-control bypass, 즉 confused-deputy 패턴에 해당합니다. Fix 이전에는 loadImageForDecoding이 완전히 attacker-controlled된 ResourceRequest를 받아들이면서 url.isValid()만 검증하고 있었습니다. 여기서 빠져 있던 invariant는 두 가지입니다. 먼저, request URL이 HTTP(S) family로 제한되지 않아 file:// URL도 검증을 통과했고, 이후 network session 쪽으로 load 요청이 전달될 수 있었습니다. 다음으로, request.firstPartyForCookies()allowsFirstPartyForCookies와 대조되어 검증된 적이 없었기 때문에, 호출자가 임의의 first-party URL을 넘기면 loader가 cookie 첨부 여부를 결정할 때 이를 그대로 신뢰하는 구조였습니다. 즉 NetworkProcess는 request 필드를 신뢰하면서도, 이를 전송 측 process의 identity에 대해 다시 도출하거나 재인가하는 과정을 거치지 않았습니다.

Exploit 방향은 regression test를 따라가면 드러납니다. Compromised WebContent process에서 m_urlfile:///path/to/targetResourceRequest를 serialize한 뒤 NetworkConnectionToWebProcess_LoadImageForDecoding을 전송하는 경우를 생각해볼 수 있습니다. 패치 이전에는 url.isValid()가 통과하고 scheme도 entry point에서 거부되지 않았으므로, 이 요청이 network session까지 진행되었을 가능성이 있습니다. 다른 경로로는, m_url은 http(s)로 유지하되 m_firstPartyForCookies에 victim origin을 지정하는 방식도 있습니다. 패치 이전에는 allowsFirstPartyForCookies가 호출되지 않았으므로 이 값 역시 그대로 신뢰되었을 것입니다. 이 필드들이 실제로 local file을 읽어 image byte로 반환하는 결과로 이어지는지, 아니면 victim의 cookie가 첨부되어 body가 반환되는 결과로 이어지는지는 이 diff에 나타나지 않는 loader 경로에 달려 있습니다. 이 취약점은 second-stage bug에 해당합니다. 즉 권한 없는 web content만으로는 도달할 수 없으며, 그 자체로 NetworkProcess 내 code execution을 부여하지도 않습니다.

이 취약점은 WebContent/NetworkProcess 사이의 IPC trust boundary와 cookie first-party 인가 모델을 약화시킵니다. Security model은 compromised WebContent process가 NetworkProcess로 하여금 임의 scheme URL을 load하게 하거나 임의의 cookie first-party를 주장하게 만들 수 없다고 가정하고 있습니다. 그런데 fix 이전의 loadImageForDecoding은 그러한 인가 없이 attacker-supplied request 필드를 그대로 재전달하고 있었습니다. 이미 WebContent에서 code execution을 확보한 attacker라면 NetworkProcess를 경유해 file:// load에 도달하고 위조된 firstPartyForCookies를 넘기는 것이 이론적으로 가능합니다. 만약 이 필드들이 실제로 loader까지 그대로 흘러간다면, local file-disclosure 및 cross-origin credentialed data-read primitive로 이어질 가능성이 있습니다.

여기서 반복적으로 확인되는 교훈은, 전체 ResourceRequest를 받아 loader로 전달하는 IPC endpoint는 구조적으로 confused-deputy에 취약하다는 점입니다. 호출자가 통제하는 필드(URL scheme, firstPartyForCookies, cookie policy)는 반드시 전송자의 identity에 대해 재인가되어야 합니다. NetworkProcess의 ambient authority가 전송자의 authority보다 크기 때문입니다. url.isValid()만으로 이루어진 게이트는 문법은 검증하지만 권한은 검증하지 못합니다.

Note: Downstream loading과 cookie-attachment 관련 결과는 필드 이름과 fix가 추가한 검사의 형태로부터 추론한 내용이며, 이 필드들을 실제로 소비하는 network-session 코드는 이번 diff에 포함되어 있지 않습니다. Entry-point의 검증 공백 자체는 diff에서 직접 확인됩니다.