[1] SetRawCookie authorization bypass enables cross-origin cookie writes
A cookie check that only asked whether the message agreed with itself.
High. Refactoring 이후 살아남은 단 하나의 검사는 공격자가 채워 넣은 두 필드를 서로 비교할 뿐입니다. 그래서 장악된 WebContent process는 자신이 지정한 origin에 대해 cookie를 기록할 수 있습니다. Memory corruption은 개입하지 않으며, 그 권한 자체가 곧 버그입니다.
WebKit에서 cookie 기록은 권한 경계를 가로지르는 동작입니다. Sandbox 안의 renderer가 요청을 보내면, 실제 cookie store를 소유한 더 높은 신뢰 수준의 process가 허용 여부를 결정합니다. 이때 그 판단을 담당하는 쪽이 NetworkConnectionToWebProcess입니다. NetworkProcess 안에서 web process 하나당 하나씩 생성되는 수신 객체로, 해당 연결로 들어오는 cookie 읽기·쓰기 메시지를 전부 처리합니다. SetRawCookie 메시지가 싣고 오는 필드 가운데 NetworkProcess가 독립적으로 알고 있는 값과 대조할 수 있는 것은 firstParty 하나뿐입니다. 대조 대상은 UI process가 frame commit 시점마다 채워 넣는 first-party origin allow-list입니다.
관전 포인트: 장악된 renderer는 자신이 실제로 호스팅하지 않는 origin에 대해 cookie를 설정할 수 있습니다. 정당하게 보유한 first party를 지정해 두고, cookie의 URL과 domain만 피해자 쪽으로 향하게 만드는 방식입니다.
Commit message는 이 메시지에서 NetworkProcess가 독자적으로 뒷받침할 수 있는 값은 firstParty뿐이라고 명시합니다. 동시에 수신 측 검사가 왜 완화되었는지도 설명합니다. InspectorPageAgent::setCookie가 cookie 하나를 모든 frame의 document에 일괄 전달했던 것이 원인입니다. 그래서 cross-origin iframe을 포함한 페이지를 inspect하면, document->cookieURL()과 document->firstPartyForCookies()가 어긋나는 반복이 최소 한 번은 반드시 발생했습니다. 정당한 송신자가 올바른 수신 측 검사에 걸리는 상황이었던 셈입니다.
패치 이전 handler에는 domain 관련 조건이 정확히 하나만 있었습니다.
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
// Pre-fix: the only domain check on the SetRawCookie path.
RegistrableDomain::uncheckedCreateFromHost(cookie.domain).matches(url)
Patch Details
변경은 수신 측과 송신 측 양쪽으로 나뉩니다. 먼저 수신 측에서는 필드끼리 비교하던 단일 조건을 두 개의 조건으로 변경했습니다. 공격자가 채워 넣는 cookie.domain과 url을 각각 firstParty에 고정시키는 형태이며, commit message에 따르면 320304@main 이전에 존재하던 조건 쌍을 복원한 것입니다. firstParty는 이미 validateCookieAccess가 allowsFirstPartyForCookies를 거쳐 검증하는 필드입니다. 이때 대조되는 상태는 UI process가 NetworkProcess::AddAllowedFirstPartyForCookies로 채워 넣습니다. 송신 측에서는 InspectorPageAgent::setCookie가 cookie 하나를 모든 frame의 document로 뿌리는 동작을 중단합니다. 대신 cookieURL()과 firstPartyForCookies()가 일치하지 않는 frame은 건너뜁니다. 그래서 in-tree 송신자를 위해 수신 측을 느슨하게 유지할 필요가 없어졌습니다. 여기에 IPC 테스트 케이스가 하나 더 추가되었는데, 기존 두 케이스가 놓쳤던 필드 조합을 정확히 겨냥합니다. cross-origin cookie.domain만 단독으로 쓰는 경우와 cross-origin url만 단독으로 쓰는 경우는 이미 검증되고 있었고, 둘이 서로 맞아떨어지는 쌍은 빠져 있었습니다.
수신 측이 독자적으로 뒷받침할 수 있는 단 하나의 필드에 고정하는 대신, 공격자가 채워 넣은 두 필드를 서로 비교하도록 작성된 IPC 권한 검사.
Background
이 코드가 있는 위치. WebKit은 작업을 네 가지 process 역할로 나눕니다. WebContent는 신뢰할 수 없는 페이지 script와 layout을 실행하고, GPU process는 rendering과 media를 담당합니다. Networking process는 socket과 공유 cookie store를 소유하며, UI process는 애플리케이션을 담당하면서 정책을 중개합니다. Cookie가 Networking process에 놓인 이유도 여기에 있습니다. Renderer가 장악되더라도 공격자가 cookie 저장소 자체를 곧바로 손에 넣지는 못하게 하기 위함입니다.
연결별 수신 객체.
WebContent process 하나마다 NetworkProcess 안에 NetworkConnectionToWebProcess가 하나씩 생성됩니다. 연결 하나에 수신 객체 하나가 묶이는 구조이며, 해당 renderer의 cookie 메시지를 전담합니다. 반대편 renderer는 완전히 장악되었을 수 있으므로, 모든 메시지의 모든 필드는 신뢰할 수 없는 입력으로 다뤄야 합니다. 수신 측이 할 일은 그중 어떤 필드를 자신이 독립적으로 아는 값과 연결지을 수 있는지 판단하는 것입니다.
First-party allow-list.
신뢰할 수 있는 대조 상대를 가진 필드는 firstParty 하나뿐입니다. Frame이 commit될 때마다 UI process는 NetworkProcess::AddAllowedFirstPartyForCookies를 전송하며, 해당 web process가 어떤 first-party origin을 대신해 동작할 자격이 있는지 명시합니다. validateCookieAccess는 allowsFirstPartyForCookies를 통해 그 상태를 조회합니다. 보유하지 않은 first party를 지정한 메시지는 Allow가 아닌 접근 결과, 또는 CookieAccess::Terminate로 이어집니다.
Registrable domain.
RegistrableDomain은 host를 public suffix + 1 형태로 축약합니다. 그래서 login.example.co.uk와 www.example.co.uk가 동일하게 비교됩니다. uncheckedCreateFromHost는 host 문자열 하나만 받아서 객체를 생성하며, 그 문자열이 어디서 왔는지는 검증하지 않습니다. Host를 신뢰할 수 있는 상황에서는 적절한 primitive지만, 신뢰할 수 없는 값 위에 보안 조건을 세울 때는 잘못된 선택입니다.
Analysis
Bug class는 process 경계를 넘는 confused deputy이며, 단일 조건으로 축약된 권한 검사 형태로 나타납니다. RegistrableDomain::uncheckedCreateFromHost(cookie.domain).matches(url)의 두 피연산자는 모두 같은 메시지에, 같은 신뢰할 수 없는 송신자로부터 도착합니다. 따라서 이 조건이 보장하는 것은 메시지가 내부적으로 일관적이라는 사실뿐입니다. 송신자가 권한을 가졌는지에 대해서는 아무것도 말해주지 않습니다. Commit message에 따르면 320304@main 이전의 두 조건은 각각 이 필드 하나씩을 firstParty에 고정하고 있었습니다. 즉 둘을 필드 대 필드 비교 하나로 합치면서, 메시지와 신뢰할 수 있는 기준값 사이의 유일한 연결고리가 사라진 셈입니다.
WebContent (compromised) NetworkProcess
──────────────────────── ──────────────
SetRawCookie { ───► validateCookieAccess(firstParty)
firstParty: attacker.test └─ UIProcess-populated allow-list OK
url: victim-bank.test
cookie.domain: victim-bank.test RegistrableDomain(cookie.domain)
} .matches(url) OK
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
│ both operands from the message
└──► privileged cookie store write
새로 추가된 IPC 테스트는 위조 형태를 그대로 코드로 옮겨 놓았습니다. firstParty: location.origin, url: 'http://victim-bank.test/', cookie.domain: 'victim-bank.test' 세 필드를 공격자가 직접 채웁니다. firstParty에는 장악된 process가 정당하게 호스팅하는 origin을 지정하므로, validateCookieAccess는 Allow를 반환하고 종료로 이어지지 않습니다. 한편 url과 cookie.domain은 둘 다 피해자를 가리키기 때문에 registrable domain이 서로 같아집니다. 위조된 쌍이 자기 자신과 일치하는 것입니다. 그래서 유일한 조건이 통과하고, 실행은 권한이 필요한 기록 경로로 그대로 흘러갑니다.
Exploit 가능성은 heap 상태나 timing 조건에 전혀 의존하지 않습니다. 필요한 것은 WebContent process를 장악한 상태와, 직접 고른 문자열 세 개를 담은 IPC 메시지 하나를 보낼 수 있는 능력뿐입니다. Renderer 장악 이후의 표준적인 위치에 해당합니다. 이렇게 확보되는 primitive는 임의의 registrable domain을 대상으로 한 cookie 기록이며, httpOnly와 secure 플래그까지 공격자가 지정합니다. 피해자 origin에서 실행되는 script조차 document.cookie로는 설정할 수 없는 cookie가 여기에 포함됩니다. 이것으로 무엇을 할 수 있는지는 대상 사이트의 session 처리 방식에 달려 있습니다. 클라이언트가 제시한 session identifier를 사이트가 받아들이는 경우에는 session fixation이 가능합니다. Token이 cookie에 담겨 있는 구조라면 double-submit CSRF 방어가 무력화됩니다. 로그인 상태가 cookie 하나로 결정되는 경우에는 forced-login 조작으로 이어질 수 있습니다.
이 vulnerability는 어떤 renderer가 어떤 origin을 대신할 수 있는지 판정하는 NetworkProcess의 역할을 약화시킵니다. Cookie store는 session 안의 모든 origin이 공유합니다. 자기 자신과의 일관성만 확인하는 권한 검사를 두면, 메시지를 조작할 수 있는 process 누구에게나 그 저장소가 기록 가능한 상태로 남습니다. 네 개 process로 나눈 구조가 지키려던 경계가 바로 이 지점입니다.
Audit directions
- 자기 일관성만 확인하는 IPC 조건. 피연산자가 전부 같은 메시지에서 나오는 수신 측 검사는 형식을 확인할 뿐 권한을 확인하지 못합니다. 위험한 형태는 두 가지입니다. 디코딩된 struct 하나의 두 필드를 비교하거나, struct 필드와 같은 레벨의 파라미터를 비교하면서, 수신 측이 독립적으로 조달할 수 있는 제3의 항이 없는 경우입니다. 출발점으로는
NetworkConnectionToWebProcess에 있는 나머지 cookie·storage 메시지를 살펴볼 만합니다. IPC로 도착한 문자열에 대해RegistrableDomain::uncheckedCreateFromHost계열을 호출하는 handler도 마찬가지입니다. 코드 리뷰에서 눈에 띄는 신호는 이렇습니다.matches(),isSameOrigin(),==의 좌변과 우변이 모두 메시지 파라미터 목록에서 도달 가능한 경우입니다. - In-tree 송신자를 수용하려고 완화된 수신 측 검사. 정당한 내부 호출자가 올바른 수신 측 검사에 걸릴 때, 안전한 수정 대상은 송신자입니다. 수신 측을 느슨하게 풀면 그 연결에 붙은 다른 모든 송신자에게도 조용히 같은 권한이 다시 열립니다. Web Inspector backend agent 전반을 점검할 필요가 있습니다. 프로토콜 명령 하나를 여러 document와 frame으로 펼쳐 보내는 in-tree 송신자들이기 때문입니다. 그리고 commit 이력상 송신자를 좁히는 대신 수신 측 검사를 넓혀 온 흔적이 있는 검사를 찾아보십시오.
- 신뢰할 수 있는 대조 상대가 있는 필드와 없는 필드. 권한이 필요한 기록을 수행하는 cross-process 메시지마다, 수신 측이 UI process가 채워 넣은 상태와 대조할 수 있는 필드와 그렇지 못한 필드를 각각 나열해 보십시오. 그런 다음 권한 조건이 전자에 고정되어 있는지 확인하면 됩니다. Cookie 경로에서 거슬러 올라갈 기준점은
NetworkProcess::AddAllowedFirstPartyForCookies입니다. 다른 subsystem에도 각각의 대응물이 존재합니다. 그리고 그중 어느 것에도 닿지 않는 메시지가 오히려 흥미로운 대상입니다.