Subresource Integrity(SRI)로 CDN 변조 막기: Polyfill.io가 알려준 교훈

2026-05-22 hit count image

2024년 Polyfill.io 사고는 CDN으로 로드하는 외부 스크립트가 어떻게 10만 개 이상의 사이트를 한 번에 감염시킬 수 있는지 보여줬습니다. Subresource Integrity(SRI)의 동작 원리와 적용 방법, 그리고 Google Fonts 같은 동적 CSS에서 SRI를 어떻게 우회·대체할지 정리합니다.

web

들어가며

공급망 시리즈의 cooldown·Hardened Mode·CodeQL은 모두 npm 레지스트리로 들어오는 의존성에 대한 방어였습니다. 그런데 현대 웹 앱이 외부 코드를 끌어오는 통로는 npm만이 아닙니다.

<script src="https://cdn.example.com/widget.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/...">

이 한 줄의 <script> 또는 <link> 가 npm 게이트를 완전히 우회합니다. 그리고 이 통로를 통해 발생한 2024년의 대형 사고가 Polyfill.io 사건입니다. 이 글은 그 사고와, 브라우저 차원의 방어 메커니즘인 Subresource Integrity(SRI) 를 다룹니다.

어떤 위험이 있는가: CDN은 신뢰의 단일 지점

<script src="https://cdn.example.com/widget.js"> 을 페이지에 넣는 순간, 다음 가정에 의존하게 됩니다.

  1. cdn.example.com 도메인이 우리가 신뢰하는 운영자의 통제 아래 있다
  2. 그 운영자의 인프라가 침해되지 않는다
  3. 그 운영자가 의도적으로 악성 코드를 삽입하지 않는다

이 세 가정 중 하나라도 무너지면, 우리 페이지를 방문하는 모든 사용자의 브라우저에서 외부 통제 코드가 실행됩니다. 그리고 무너지는 사례는 의외로 흔합니다.

  • 도메인 매각/인수: 원래 운영자가 도메인을 팔고, 새 소유자가 악성으로 운영
  • DNS/CDN 침해: 도메인은 그대로지만 응답하는 콘텐츠가 변조
  • 메인테이너 사보타주: 의도적으로 악성 코드 삽입
  • CDN 인프라 침해: 운영자도 모르는 사이 콘텐츠가 변조

가장 무서운 점은, npm과 달리 CDN 응답은 매 요청마다 받아오기 때문에 cooldown 같은 시간 기반 방어가 통하지 않는다는 것입니다. 변조된 순간부터 모든 사용자가 즉시 영향을 받습니다.

실제 사례: Polyfill.io (2024)

Polyfill.io는 한때 사실상의 표준 polyfill CDN이었습니다. 100,000개 이상의 웹사이트가 <script src="https://cdn.polyfill.io/v3/polyfill.min.js"> 한 줄로 이 서비스를 사용하고 있었습니다.

사고의 전말은 다음과 같습니다.

  1. 2024년 2월: 중국 기업 Funnull이 polyfill.io 도메인과 GitHub 계정을 인수
  2. 2024년 6월 24일: Sansec이 cdn.polyfill.io가 사용자의 모바일 디바이스에 악성 코드를 삽입하고 있다는 사실을 공개
  3. 악성 코드는 선택적으로 동작 — 일부 디바이스·시간대·관리자 페이지에서는 잠복, 일반 사용자에게만 활성화 (탐지 회피)
  4. 영향 사이트는 100,000개 이상 — JSTOR, Intuit, Booking.com 등 포함
  5. Cloudflare는 cdn.polyfill.io를 실시간으로 자사 안전 버전으로 리라이트, Namecheap은 도메인을 보류 처리

이 사고의 본질은 단순합니다.

운영자가 바뀌었지만, <script> 한 줄은 그대로였다.

코드 한 줄도 바뀌지 않은 사이트들이 한꺼번에 감염되었습니다. 이 사고가 보여준 것은 “외부 호스트의 신뢰성은 시간이 지나면서 변할 수 있고, 우리는 그것을 자동으로 감지할 수 없다”는 점입니다.

Subresource Integrity 동작 방식

Subresource Integrity (SRI)는 이 문제를 브라우저 차원에서 해결하는 W3C 표준입니다.

원리는 단순합니다. <script> 또는 <link> 태그에 콘텐츠의 해시 값을 함께 적어 두면, 브라우저는 다운로드받은 콘텐츠의 해시가 그것과 일치할 때에만 실행/적용합니다.

<script
  src="https://cdn.example.com/widget.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>
  • integrity: 정상 콘텐츠의 해시 (sha256/sha384/sha512)
  • crossorigin: SRI 검증에 필요한 CORS 모드

이 상태에서 만약 cdn.example.com이 변조되어 다른 콘텐츠를 응답하면, 브라우저는 해시 불일치를 감지하고 스크립트 실행을 거부합니다. 콘솔에 보안 에러가 찍히고, 그 스크립트의 코드는 전혀 실행되지 않습니다.

Polyfill.io 사고 시점에 SRI가 적용되어 있던 사이트들은 어땠을까요? 해시가 변조된 콘텐츠와 맞지 않아 자동으로 차단되었습니다. 사이트가 polyfill 기능을 잃을 뿐, 사용자 데이터는 안전합니다.

어떻게 적용하는가

1. 해시 생성

다음과 같이 해시를 계산할 수 있습니다.

curl -s https://cdn.example.com/widget.js \
  | openssl dgst -sha384 -binary \
  | openssl base64 -A

또는 온라인 도구로 srihash.org 같은 사이트를 쓸 수도 있습니다.

2. HTML 태그에 적용

- <script src="https://cdn.example.com/widget.js"></script>
+ <script
+   src="https://cdn.example.com/widget.js"
+   integrity="sha384-<해시>"
+   crossorigin="anonymous"
+ ></script>

3. Vite/webpack 플러그인으로 자동화

번들러를 통해 외부 자산이 결정된다면 SRI를 자동 삽입하는 플러그인이 있습니다.

이런 플러그인을 쓰면 빌드 시점에 자동으로 해시가 계산되어 HTML에 삽입됩니다. 외부 CDN 자산도 빌드 시점에 한 번 다운로드해서 해시를 계산할 수 있습니다.

Google Fonts처럼 SRI를 쓸 수 없는 경우

Google Fonts CSS는 SRI를 적용하기가 까다롭습니다. 그 이유는:

Google Fonts CSS는 요청한 브라우저의 User-Agent에 따라 동적으로 다른 콘텐츠를 응답합니다. 같은 URL이라도 Chrome과 Safari에 다른 @font-face 규칙이 반환됩니다.

해시는 고정값이어야 하는데 응답이 매번 다르므로, SRI를 적용해도 매번 검증 실패가 발생합니다.

대안: 폰트 자체 호스팅

가장 깔끔한 해결책은 폰트를 번들에 포함시키는 것입니다.

yarn add @fontsource/noto-sans-jp
// main.tsx
import '@fontsource/noto-sans-jp/400.css'
import '@fontsource/noto-sans-jp/700.css'

이렇게 하면 다음 이점이 모두 따라옵니다.

  • SRI를 적용할 필요 자체가 없어짐 (외부 호스트가 사라짐)
  • 사용자 IP가 Google로 전송되는 GDPR 이슈가 해결됨
  • 폰트 로딩 속도 향상 (CDN ↔ 우리 도메인 간 핸드셰이크 제거)
  • 오프라인 환경에서도 동작

같은 패턴이 @fontsource/inter, @fontsource/roboto 등 거의 모든 Google Font에 대해 npm 패키지로 제공됩니다.

한계

  • SRI는 콘텐츠 변경 자체를 막지 않습니다. 콘텐츠가 변경되면 브라우저가 그것을 실행하지 않을 뿐, 정상 콘텐츠도 함께 동작을 멈춥니다. 즉, 가용성을 희생해서 무결성을 확보하는 트레이드오프입니다.
  • 버전 업데이트마다 해시 갱신 필요. 외부 자산이 업데이트되면 해시도 업데이트해야 합니다. 빌드 자동화가 없으면 운영 부담이 됩니다.
  • 동적 콘텐츠에는 적용 불가. Google Fonts CSS, Tag Manager, 광고 SDK 등 매번 다른 응답을 주는 자산은 SRI 적용이 불가능합니다. 이 경우는 자체 호스팅이 유일한 답입니다.
  • CDN 자체의 보안 사고는 막지만, CDN과 함께 묶여 있는 다른 신뢰 가정(예: 도메인을 통한 쿠키 노출)은 별개로 다뤄야 합니다.

마무리

SRI는 한 줄짜리 변경으로 가장 큰 종류의 외부 코드 공격(CDN 변조) 한 가지를 무력화시키는, 매우 ROI가 높은 방어입니다. Polyfill.io 사고에서 SRI를 적용한 사이트와 그렇지 않은 사이트가 받은 피해의 차이는 결정적이었습니다.

권장 단계:

  1. 현재 외부에서 로드하는 모든 자산을 조사한다<script src="https://..."> , <link href="https://...">
  2. 고정 콘텐츠는 SRI 해시 추가 — 빌드 시점에 자동화 가능
  3. 동적 콘텐츠(Google Fonts 등)는 자체 호스팅으로 전환 — fontsource·자체 정적 호스팅
  4. 번들러 플러그인으로 자동화 — 외부 자산이 추가될 때마다 해시가 자동 갱신

다음 Polyfill.io급 사고가 일어났을 때 사용자 데이터를 지키느냐 마느냐는, 오늘 이 한 줄을 추가하느냐에 달려 있습니다.

참고 자료

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS