← All reports

[1] CORS bypass via unvalidated SetCORSDisablingPatterns IPC

HighWebKit multi-process IPC layer (NetworkProcess)CrossOrigin

The CORS policy took a detour through the process it was written to contain.

841ad59

High. CORS enforcement 여부를 network layer에 알려주는 policy가, 원래 그 policy로 제약해야 할 sandboxed process를 거쳐 enforcement point까지 전달되었습니다. Payload validation만으로는 이런 구조적 문제를 막을 수 없었습니다. 라우팅 자체가 버그였던 셈이며, code execution을 확보한 renderer는 메시지 한 번으로 인증된 모든 사이트를 universal하게 읽어낼 수 있는 경로를 갖게 되었습니다.

WebKit은 작업을 신뢰된 UIProcess, untrusted script를 실행하는 sandboxed WebContent process, 그리고 cookie jar를 소유하고 실제 HTTP load를 수행하는 NetworkProcess로 나누어 처리합니다. Embedder는 페이지별로 URL match pattern 목록 — _corsDisablingPatterns 설정 — 을 WebKit에 전달할 수 있으며, 이 패턴에 해당하는 URL에서는 CORS enforcement가 건너뛰어집니다. 이 목록은 renderer(WebCore 쪽의 same-origin check용)와 network layer(credential이 포함된 cross-origin response를 거부할지 결정하는 쪽) 양쪽 모두에 전달되어야 합니다. 보안상 기대되는 전제는, NetworkProcess에서 enforcement를 좌우하는 값이 최소한 그 enforcement point만큼의 privilege를 가진 process에 의해서만 attest되어야 한다는 것입니다.

관전 포인트: 이미 code execution을 확보한 renderer는 *://*/*를 선언하는 메시지 하나만 보내면, network layer가 모든 URL에 대해 CORS enforcement를 중단하도록 만들 수 있습니다. 결과적으로 renderer compromise가, 사용자가 로그인된 모든 사이트를 읽어낼 수 있는 universal read로 확장됩니다.

Commit message에는 패치 이전 경로가 명확히 기술되어 있습니다. _corsDisablingPatterns는 UIProcess에서 WebContent process를 거쳐 Messages::NetworkConnectionToWebProcess::SetCORSDisablingPatterns를 통해 NetworkProcess로 전달되었습니다. 이 구조에서는 compromise된 WebContent process가 공격자가 원하는 pattern을 담아 해당 IPC를 보내면, 임의의 cross-origin URL에 대해 CORS를 비활성화하고 사용자가 인증된 모든 사이트의 content를 읽어낼 수 있었습니다. 패치는 pattern을 UIProcess에서 NetworkProcess로 직접 전달하도록 라우팅을 변경해 WebContent process를 trust path에서 제외했습니다. 다만 Messages::WebPage::UpdateCORSDisablingPatterns는 그대로 유지되어, renderer가 자신의 in-process origin-access pattern은 계속 채울 수 있습니다.

Source/WebKit/NetworkProcess/NetworkProcess.cpp

@@ NetworkProcess::createNetworkConnectionToWebProcess
+ // connection 생성 시점에 UIProcess가 전달한 CORS-disabling pattern을 적용합니다. 이는 NetworkProcess가
+ // 시작되기 전에 WebPageProxy에 _corsDisablingPatterns가 설정되어 SetCORSDisablingPatternsForPage IPC가
+ // 이 process까지 도달하지 못했던 경우를 처리하기 위함입니다.
+ for (auto& [pageIdentifier, patterns] : parameters.corsDisablingPatternsPerPage)
+ setCORSDisablingPatternsForPage(identifier, pageIdentifier, WTF::move(patterns));
...
-void NetworkProcess::setCORSDisablingPatterns(NetworkConnectionToWebProcess& connection, PageIdentifier pageIdentifier, Vector<String>&& patterns)
+void NetworkProcess::setCORSDisablingPatternsForPage(WebCore::ProcessIdentifier webProcessIdentifier, PageIdentifier pageIdentifier, Vector<String>&& patterns)
{
+ // 이 메시지는 WebProcess가 아니라 UIProcess에서 직접 전송됩니다. Compromise된 WebContent process가
+ // NetworkProcess 쪽에서 CORS를 비활성화할 수 있다면, 임의의 cross-origin site의 content를 읽어낼 수
+ // 있기 때문입니다.
auto parsedPatterns = WTF::compactMap(WTF::move(patterns), [&](auto&& pattern) -> std::optional<UserContentURLPattern> {
UserContentURLPattern parsedPattern(WTF::move(pattern));
 
- if (parsedPattern.isValid()) {
 
- connection.originAccessPatterns().allowAccessTo(parsedPattern);
 
- return parsedPattern;
 
- }
 
- return std::nullopt;
+ if (!parsedPattern.isValid())
+ return std::nullopt;
+ if (RefPtr connection = webProcessConnection(webProcessIdentifier))
+ connection->originAccessPatterns().allowAccessTo(parsedPattern);
+ return parsedPattern;
});

Source/WebKit/WebProcess/WebPage/WebPage.cpp

@@ WebPage::updateCORSDisablingPatterns
m_corsDisablingPatterns = WTF::move(patterns);
 
- synchronizeCORSDisablingPatternsWithNetworkProcess();
page->setCORSDisablingPatterns(parseAndAllowAccessToCORSDisablingPatterns(m_corsDisablingPatterns));
}
 
-void WebPage::synchronizeCORSDisablingPatternsWithNetworkProcess()
-{
 
- // FIXME: 이 메커니즘은 아마도 UIProcess와 NetworkProcess 사이에서 직접 처리하는 편이 나을 것입니다.
 
- WebProcess::singleton().ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::SetCORSDisablingPatterns(m_identifier, m_corsDisablingPatterns), 0);
-}

이번 변경은 WebContent에서 Networking으로 향하던 전달 경로를 제거하고, 대신 UIProcess에서 Networking으로 향하는 경로를 새로 추가합니다. 아울러 아직 NetworkProcess가 존재하지 않는 경우를 위해, 패턴 목록이 connection 생성 파라미터를 통해 함께 전달되도록 합니다.

제거된 항목은 다음과 같습니다. NetworkConnectionToWebProcess.messages.inSetCORSDisablingPatterns(WebCore::PageIdentifier, Vector<String>) 엔트리(DispatchedFrom=WebContent, DispatchedTo=Networking로 주석된 receiver block), 해당 핸들러인 NetworkConnectionToWebProcess::setCORSDisablingPatterns와 그 선언부, WebPage::synchronizeCORSDisablingPatternsWithNetworkProcess()와 이를 호출하던 세 지점(WebPage 생성자, WebPage::~WebPage, WebPage::updateCORSDisablingPatterns), 그리고 WebProcess::ensureNetworkProcessConnection()에서 새로 생성된 각 network connection에 살아있는 모든 WebPagem_corsDisablingPatterns를 다시 게시하던 loop입니다.

새로 추가된 항목은 다음과 같습니다. NetworkProcess.messages.inSetCORSDisablingPatternsForPage(WebCore::ProcessIdentifier, WebCore::PageIdentifier, Vector<String>)가 추가되었습니다. NetworkProcess::setCORSDisablingPatterns(NetworkConnectionToWebProcess&, ...)setCORSDisablingPatternsForPage(ProcessIdentifier, PageIdentifier, Vector<String>&&)로 이름이 바뀌었고, 이제 dispatch 메커니즘으로부터 connection을 전달받는 대신 webProcessConnection(webProcessIdentifier) — null을 허용하는 RefPtr — 를 통해 스스로 connection을 조회합니다. WebPageProxy::sendCORSDisablingPatternsToNetworkProcessIfNecessary()가 새로 추가되어 WebPageProxy::setCORSDisablingPatterns()WebPageProxy::finishAttachingToWebProcess() 양쪽에서 호출됩니다. 후자는 process swap 상황을 처리하는 지점으로, legacyMainFrameProcess().coreProcessIdentifier()webPageIDInMainFrameProcess()를 전달합니다.

launch 이전 상황을 위해서는, NetworkProcessConnectionParametersHashMap<WebCore::PageIdentifier, Vector<String>> corsDisablingPatternsPerPage 필드가 추가되었습니다(헤더와 .serialization.in 양쪽). 이 필드는 NetworkProcessProxy::getNetworkProcessConnection()에서 page->corsDisablingPatterns()를 통해 채워지며, NetworkProcess::createNetworkConnectionToWebProcess()에서 적용됩니다.

보안 정책이 그 정책의 적용 대상이 되는 컴포넌트를 거쳐 시행 지점까지 전달되는 구조입니다. 그 컴포넌트가 침해되면 정책 자체가 위조될 수 있습니다.

이 코드가 있는 위치. WebKit의 IPC 계층은 UIProcess(신뢰되는 애플리케이션 프로세스), 하나 이상의 sandboxed WebContent process, 그리고 NetworkProcess 사이에서 메시지를 전달합니다. WebContent는 renderer 버그가 exploit되는 순간 완전히 attacker 통제 하에 놓인다고 간주됩니다. UIProcess는 신뢰되는 프로세스이며, NetworkProcess는 cookie jar와 credential을 보유한 채 실제 network I/O를 수행합니다.

메시지 선언과 annotation. IPC endpoint는 .messages.in 파일에 선언됩니다. receiver block에는 DispatchedFrom=WebContent, DispatchedTo=Networking과 같은 annotation이 붙을 수 있으며, 이는 해당 block의 메시지를 어느 프로세스가 보낼 수 있는지를 명시합니다. MESSAGE_CHECK 매크로는 receiver에서 IPC 인자를 검증하고, 위반 시 발신자의 connection을 종료시키는 WebKit의 관례입니다.

Connection 객체. NetworkConnectionToWebProcess는 하나의 WebContent process connection을 나타내는 NetworkProcess 측 객체이며, NetworkProcess::webProcessConnection(WebCore::ProcessIdentifier)가 이를 조회합니다. NetworkProcessConnectionParameters는 UIProcess가 NetworkProcessProxy::getNetworkProcessConnection()을 통해 채운 뒤, WebProcess의 network connection이 수립될 때 NetworkProcess::createNetworkConnectionToWebProcess()로 전달하는 구조체입니다.

CORS-disabling patterns. 이는 페이지별 embedder 설정으로, _corsDisablingPatterns로 노출되며 WebPageProxy::setCORSDisablingPatterns를 통해 WebKit에 도달합니다. 각 문자열은 WebCore::UserContentURLPattern으로 파싱되는데, 이때 isValid()는 해당 문자열이 형식상 올바른 pattern인지만 보고합니다 — *://*/*는 모든 scheme, host, path에 매치됩니다. network 측에서는 파싱된 pattern이 NetworkProcess::shouldDisableCORSForRequestTo(PageIdentifier, URL)에서 참조되며 connection의 NetworkOriginAccessPatterns에 등록됩니다. WebCore 측에서는 동등한 목록이 Page::setCORSDisablingPatterns()를 통해 설치됩니다.

식별자. WebCore::PageIdentifier는 WebContent process 내부의 페이지를 지칭하고, WebCore::ProcessIdentifier는 WebContent process 자체를 지칭합니다. 둘 다 IPC payload에 담기는 단순한 ObjectIdentifier 정수입니다.

Process swap. 페이지는 navigation 과정에서 다른 WebContent process로 옮겨질 수 있습니다. WebPageProxy::finishAttachingToWebProcess()는 페이지가 (새로 바뀌었을 수도 있는) WebProcess에 attach된 이후 UIProcess에서 실행되며, 페이지별 상태가 다시 수립되는 지점입니다.

pattern 목록은 원래 UIProcess에서 신뢰되는 embedder 설정으로 만들어지지만, NetworkProcess에는 오직 renderer를 경유해서만 도달했습니다.

  Before:                                    After:
  UIProcess                                  UIProcess
    └─► WebPage::UpdateCORSDisablingPatterns    ├─► WebPage::UpdateCORSDisablingPatterns
          │  (WebContent)                       │     (WebContent, in-process checks only)
          └─► synchronizeCORSDisabling...()     └─► NetworkProcess::SetCORS...ForPage
                └─► NetworkConnectionToWebProcess              │
                    ::SetCORSDisablingPatterns   ◄── forgeable └─► enforcement state
                          └─► enforcement state

핸들러인 NetworkConnectionToWebProcess::setCORSDisablingPatterns는 인자를 아무런 MESSAGE_CHECK 없이 그대로 NetworkProcess::setCORSDisablingPatterns로 전달했습니다. pattern 문자열에 대해서도 (참조된 것은 UserContentURLPattern::isValid()뿐이었고, 이는 authorization check가 아니라 syntax check에 불과합니다), 그리고 전달된 PageIdentifier가 해당 connection이 호스팅하는 페이지에 속하는지에 대해서도 검증이 이루어지지 않았습니다. 여기서 빠져 있던 invariant는, NetworkProcess가 CORS 시행을 건너뛸지 여부를 좌우하는 값이라면 최소한 시행 지점만큼 privilege가 높은 프로세스에 의해 attest되어야 한다는 원칙입니다. 이 값을 sandboxed renderer를 거쳐 전달한 것은, renderer를 제약하기 위해 존재하는 정책에 대해 오히려 renderer의 주장을 권위 있는 것으로 만든 셈입니다.

이 취약점은 web content에서 직접 도달할 수 있는 경로는 아닙니다. endpoint가 IPC message이기 때문에, attacker는 먼저 별도의 renderer 버그를 통해 WebContent process 내에서 arbitrary code execution(최소한 arbitrary IPC-send 능력)을 확보해야 합니다. 이 조건이 갖춰지면, trigger 자체는 기존 network connection 위에서 이루어지는 단 한 번의 미인증 write입니다. SetCORSDisablingPatterns(pageID, { "*://*/*" }).

이 문자열은 형식상 올바르므로 isValid()가 true를 반환하고, connection.originAccessPatterns().allowAccessTo(parsedPattern)을 통해 pattern이 등록됩니다. 이후 shouldDisableCORSForRequestTo(pageIdentifier, url)에서 참조되기 위해 유지되는데, 해당 유지 코드는 제시된 hunk의 끝 부분 바로 다음에 위치합니다. 다만 바로 위에 있는 page 기반 signature를 보면 이 조회가 페이지 단위로 이루어짐을 알 수 있습니다. 이후 attacker는 임의의 origin에 대해 통상적인 credentialed load(fetch(url, {credentials:'include'}), XMLHttpRequest, 혹은 직접 구성한 ScheduleResourceLoad IPC)를 발생시킬 수 있으며, network 측 cross-origin check가 생략되어 응답 본문이 침해된 renderer로 그대로 반환됩니다. renderer 측 WebCore check는 아무런 장애물이 되지 못합니다. 이미 침해된 프로세스는 자신의 address space를 통제하며, UpdateCORSDisablingPatterns 핸들러 역시 local하게 보유하고 있기 때문입니다.

이 primitive를 더 날카롭게 만드는 두 가지 특성이 있습니다. 먼저 pageIdentifier 인자가 발신 connection이 호스팅하는 페이지에 속하는지 전혀 검증되지 않았기 때문에, attacker는 자신의 것이 아닌 다른 식별자를 대상으로 삼을 수도 있었습니다. 다만 NetworkProcess가 해당 엔트리를 connection 단위가 아니라 순수하게 PageIdentifier 기준으로만 키잉하는지는 제공된 context만으로는 확인되지 않습니다. 그리고 WebProcess::ensureNetworkProcessConnection()에서 삭제된 loop는 살아있는 각 WebPagem_corsDisablingPatterns를 새로 생성되는 모든 network connection에 자동으로 다시 게시했습니다. 따라서 attacker가 자신의 address space 안에서 해당 멤버를 함께 덮어썼다면, 위조된 메시지를 다시 보낼 필요 없이 connection teardown 시점마다 permissive한 값이 자동으로 재적용되었을 것입니다.

이번 변경은 새로운 IPC entry point인 Messages::NetworkProcess::SetCORSDisablingPatternsForPage와, NetworkProcessConnectionParameters에 새로 직렬화되는 corsDisablingPatternsPerPage 필드를 추가함으로써 NetworkProcess의 attack surface를 확장시킵니다. 이제 NetworkProcess의 CORS-disabling 상태는 NetworkProcess receiver 위의 메시지를 통해서도, connection 생성 시점에도 기록될 수 있습니다. 그리고 해당 핸들러는 dispatching connection을 사용하는 대신 메시지 안에 담긴 식별자를 이용해 connection을 조회합니다.

여기에는 강제되지 않고 가정에만 의존하는 지점이 두 곳 있습니다. 하나는 NetworkProcess receiver block이 오직 parent connection 위에서만 dispatch 가능하다는 가정입니다. diff에 보이는 block header인 messages -> NetworkProcess : AuxiliaryProcess WantsAsyncDispatchMessage {에는 제시된 발췌 범위 안에서 DispatchedFrom= annotation이 붙어 있지 않으므로, 이는 WebKit의 일반적인 process-model 기대에 의존하는 상태입니다. 다른 하나는 webProcessIdentifier 인자가 정직하다는 가정으로, 핸들러가 이를 발신 connection과 대조 검증하지 않습니다. NetworkProcessConnectionParameters 역시 UIProcess에서 작성되므로 동일한 신뢰 가정이 적용됩니다.

이 가정 중 하나라도 깨진다면, 패치 이전의 bypass가 새로운 형태로 재현될 수 있습니다. 게다가 이 경우에는 attacker가 어느 connection의 originAccessPatterns에 permissive entry가 적용될지까지 선택할 수 있다는 점이 추가됩니다. 새롭게 도달 가능해진 상태 조합도 있습니다. webProcessConnection()이 null을 반환하는 동안에도 setCORSDisablingPatternsForPage가 실행되어 파싱된 pattern이 유지되는 반면, connection 단위 pattern set은 갱신되지 않는 상태입니다. 이는 connection이 dispatch 메커니즘에 의해 공급되던 기존 구조에서는 발생할 수 없었던 조합입니다.

이 취약점은 WebContent process의 sandbox 경계, 그리고 이를 통해 network 계층에서 시행되는 same-origin policy를 약화시킵니다. 보안 모델은 침해된 renderer가 더 높은 privilege를 가진 프로세스가 보유한 정책 상태에 의해 제약된다는 전제를 두고 있습니다. 패치 이전에는 이 전제가 성립하지 않았고, renderer code execution을 확보한 attacker는 임의 URL에 대해 CORS를 비활성화하여 credentialed cross-origin 응답의 본문을 읽어낼 수 있었습니다. 이는 별도의 sandbox escape 없이도 사용자가 인증되어 있는 모든 사이트(webmail, banking, internal corporate app)에 대한 universal read에 해당합니다.

삭제된 helper 함수에는 스스로의 문제를 예고하는 주석이 남아 있었습니다. // FIXME: We should probably have this mechanism done between UIProcess and NetworkProcess directly. 즉 이 안전하지 않은 라우팅 방식은, 경계 위반으로 인식되기 훨씬 전부터 이미 suboptimal한 구조로 문서화되어 있었던 셈입니다.

이 사례가 보여주는 일반적인 패턴은 policy laundering입니다. 원래 출처에서는 신뢰할 수 있던 값이, 더 낮은 privilege를 가진 프로세스를 거쳐 전달되는 순간 신뢰할 수 없는 값으로 바뀝니다. 그런데도 이를 수신하는 코드의 mental model은 여전히 이 값을 embedder 설정처럼 취급합니다. attacker input이 아니라 설정값처럼 보이기 때문에, 이 값에 대한 MESSAGE_CHECK는 애초에 작성된 적이 없습니다. 그리고 사실 작성될 수도 없었습니다. "이 pattern이 authorized된 것인가?"라는 질문은, 신뢰되는 프로세스로부터의 attestation 없이는 network 계층에서 답할 수 없는 질문이기 때문입니다. 결국 유일하게 올바른 해법은 라우팅 자체를 바꾸는 것뿐입니다.

한 가지 짚어둘 점은, NetworkConnectionToWebProcess.messages.in에는 동일한 DispatchedFrom=WebContent block 안에 origin-policy를 변경하는 성격을 가진 이웃 항목이 최소 하나 더 남아 있다는 사실입니다. 삭제된 라인 바로 위에 위치한 RegisterURLSchemesAsCORSEnabled가 그것입니다.