이 공격 전체를 한 문장으로 요약하면 이렇다: 2024년 2월, 어떤 회사가 도메인 하나를 샀다.
끝이다. 서버를 뚫은 사람은 없다. 피싱당한 자격증명도, 악용된 CVE도, 오염된 빌드 파이프라인도 없다. 누군가 polyfill.io에 — 10만 개가 넘는 웹사이트가 수년째 <script> 태그로 가리키고 있던 바로 그 도메인에 — 돈을 냈고, 그들 방문자 모두의 브라우저에서 임의의 자바스크립트를 실행할 권리를 합법적인 거래로 손에 넣었다. 넉 달 뒤, 그들은 그 권리를 썼다.
polyfill.io가 무엇이었고, 왜 다들 믿었나
폴리필(polyfill)은 옛 브라우저에 빠진 기능을 가르쳐 주는 작은 자바스크립트 조각이다 — 여기선 fetch(), 저기선 Array.flat(). 원래 앤드루 베츠(Andrew Betts)가 파이낸셜 타임스에 있을 때 만든 polyfill.io 서비스는 여기서 영리한 짓을 했다: 페이지에 스크립트 태그 하나만 넣으면, 서비스가 방문자마다 User-Agent를 들여다보고 바로 그 브라우저에 필요한 폴리필만 골라 돌려줬다. 최신 크롬은 거의 빈 파일을 받고, 낡은 브라우저는 심(shim) 한 무더기를 받는다. URL 하나, 요청마다 다른 응답.
진짜 유용했고, 공짜였고, 그래서 퍼졌다. 2024년 무렵엔 취미 블로그부터 정부 사이트, 실제 쇼핑몰 결제 페이지까지 온 데 박혀 있었다. 그 스크립트 태그는 10년 전 템플릿에 복붙된 뒤 잊혔다. 서드파티 의존성이 핵심 부품이 되는 방식이 정확히 이렇다: 결정으로가 아니라, 누적으로.
그리고 그 태그 하나하나는 브라우저에게 이렇게 지시하는 명령이었다: 이 도메인에서 코드를 받아, 누가 방문하든 매번, 내 페이지에서 전권으로 실행하라. 스크립트 태그가 하는 일을 과장한 게 아니다. 스크립트 태그는 문자 그대로 그 일을 한다.
방어가 불가능하게 만든 설계
여기서부터가 “또 하나의 공급망 사건”을 곱씹을 만한 것으로 바꾸는 대목이다. 웹에는 바로 이 위협을 막는 방어 장치가 이미 있다. Subresource Integrity(SRI)라고 하는데, 스크립트 태그에 암호학적 해시를 못박는 것이다: integrity="sha384-...". 브라우저가 스크립트를 내려받아 해시를 계산하고, 값이 안 맞으면 실행을 거부한다. 서드파티 스크립트에 SRI를 걸어두면 제공자가 당신 몰래 코드를 바꿀 수 없다 — 바꾸는 순간 해시가 깨지고 브라우저가 막는다.
polyfill.io에는 SRI를 쓸 수 없었다. 사람들이 깜빡해서가 아니라, 서비스의 핵심 세일즈 포인트가 그걸 불가능하게 만들었기 때문이다: 응답이 브라우저마다 달랐다. 요청마다 바이트가 새로 생성되는데 못박을 단일 해시가 있을 리 없다. polyfill.io를 편리하게 만든 그 기능 — 동적인, 방문자별 응답 — 이 바로, CDN이 돌변하는 걸 막으려고 설계된 유일한 브라우저 장치를 무력화한 그 기능이었다.
그래서 10만 개 사이트가 못박지도, 검증하지도 못하는, 원격 제어되는 코드를 한 도메인에서 돌리고 있었고, 그들과 재앙 사이에 선 유일한 것은 그 도메인을 누가 소유하고 있느냐였다. 신뢰가 모든 일을 하고 있었는데, 그 신뢰가 매물로 나와 있었다.
2월: 아무도 움직이지 않은 경고
소유권 이전은 비밀이 아니었고, 이 바닥 사람이라면 바로 이 대목이 불편해야 한다. 도메인과 프로젝트 깃허브 계정이 2024년 2월 한 중국 회사에 팔렸을 때(이 회사는 나중에 CDN 업체 Funnull과 연결됐다), 이걸 만든 장본인 앤드루 베츠는 즉시 공개적으로 나섰다. 메시지는 단도직입적이었다: 자신은 그 도메인을 소유한 적이 없고, 매각에 아무런 발언권도 없었으며, 당신 사이트에 cdn.polyfill.io가 들어 있다면 지금 당장 빼라. 요즘 브라우저는 폴리필이 아예 필요 없다고 그는 지적했다.
악성코드가 배포되기 몇 달 전에, 모든 신호가 공개된 채로 거기 있었다. WHOIS/RDAP 레코드가 주인을 바꿨다. 깃허브 계정이 주인을 바꿨다. 원작자가 일어나 이걸 더는 믿지 말라고 말했다. 이걸 알아채는 데 보안팀도 스캐너도 필요 없었다 — 자기 사이트가 의존하는 코드의 출처(provenance)를 지켜보는 누군가가 필요했을 뿐이다. 거의 아무도 지켜보지 않았다. 그 스크립트 태그는 10년간 보이지 않는 인프라였고, 거기 있다는 걸 아무도 기억 못 했으니 지켜보라고 배정된 사람도 없었다.
6월: 청구서가 날아들다
2024년 6월 25일, Sansec의 연구진이 cdn.polyfill.io가 자신을 임베드한 사이트들에 악성코드를 주입하고 있다고 보고했다. 페이로드는, 이게 치고 빠지는 식이 아니라 전문적인 작전임을 알려주는 방식으로 신중했다. 요청 헤더에 따라 동적으로 생성돼, 연구자가 curl로 파일을 당겨 본 것과 모바일 네트워크의 폰이 받은 것이 서로 달랐다. 모바일 기기를 노렸고 특정 시간대에만 발동했다. 관리자 사용자가 감지되면 잠잠했고, 웹 분석 서비스를 발견하면 실행을 늦췄다 — Sansec의 추정대로라면 통계에 안 잡히려고. 리버스 엔지니어링 방지 수법까지 달고 있었다.
발동할 때는 피해자를 가짜 분석 도메인 — 로그에서 놓치기 쉬운 구글 오타 사칭 www.googie-anaiytics.com — 을 거쳐 스포츠 베팅 사이트로 넘겼다. 처음 집계는 “10만 개 이상 사이트”였다. 7월 2일 Censys의 조사는 그 도메인을 참조하는 스크립트를 여전히 심고 있는 호스트를 384,773개 찾아냈다.
정리는 빨랐고, 웹의 얼마나 많은 부분이 소수의 중간자를 거치는지를 스스로 증언했다. 구글은 그 스크립트를 부르는 전자상거래 사이트의 광고를 막기 시작했다. 클라우드플레어는 자사 네트워크 뒤의 모든 사이트에서 cdn.polyfill.io 요청을 실시간으로 안전한 미러로 바꿔치기했다. 패스틀리는 다른 도메인에 깨끗한 사본을 세웠다. 등록대행사 네임칩은 며칠 안에 악성 도메인을 정지시켰다. 이 공격을 끝낸 것은 10만 명의 사이트 주인들 — 대부분은 끝까지 몰랐다 — 이 아니라, 엣지에서 스크립트 태그를 덮어쓸 영향력을 가진 소수의 인프라 회사들이었다.
교훈은 코드가 아니라 소유권에 관한 것이다
이걸 “의존성을 감사하라”로 분류하고 싶겠지만, 그 틀은 실제로 벌어진 그 특정한 것을 놓친다. 취약점은 코드가 아니었다. 코드는 수년간 멀쩡했다. 취약점은 코드를 바꿀 권리가 사고팔 수 있는 자산이었다는 것, 그리고 거기 의존하던 사람들에게는 그 권리가 넘어갈 때 알아챌 수단이 — 기술적으로도 조직적으로도 — 없었다는 것이다.
당신이 통제하지 않는 도메인을 가리키는 <script src>는, 버전을 못박은 라이브러리 같은 의미의 의존성이 아니다. 그것은 코드 실행에 대한 상시 위임이고, 페이지가 로드될 때마다 갱신되며, 도메인을 쥔 자가 언제든 회수할 수 있고, 소유권 증서와 함께 조용히 양도된다. 당신이 믿는 것은 한 덩이의 코드가 아니라, 그 호스트명에 응답하는 누군가와의 지속되는 관계다 — 그리고 도메인은, 코드와 달리, 늘 조용히 그리고 합법적으로 주인을 바꾼다.
이걸 실제로 다루는 방어책은 화려하지 않다. 자체 호스팅할 수 있는 서드파티 스크립트는 자체 호스팅해서, 딴 데서 일어난 매각이 당신 사용자의 브라우저에 손을 뻗지 못하게 하라. 제공자가 정적 바이트를 주는 곳이면 어디든 SRI로 못박고, “동적이라 못박을 수 없다”는 말을 어깨 한 번 으쓱하고 넘길 게 아니라 큰 경고음으로 받아들여라. 그리고 — 업계가 가장 못하는 부분인데 — 당신이 의존하는 것의 출처를 계속 지켜봐라: 당신 스크립트 태그 속 도메인을 누가 소유하는지, 그 답이 당신이 처음 신청했던 그 답 그대로인지. 폴리필 공격에는 제로데이가 필요 없었다. 모두가 10년간 주의를 끊기만 하면 됐고, 그건 훨씬 더 믿을 만한 익스플로잇이다.