서드파티 스크립트 공급망 공격 방어 실전: SRI 해시·CSP 소스 화이트리스트·npm 의존성 감사로 CDN 오염 막기

CDN이 신뢰할 수 없게 된 날: 공급망 공격은 이미 우리 곁에 있다
웹 서비스는 오래전부터 외부 CDN에서 jQuery, Bootstrap, Google Analytics를 불러왔습니다. 빠르고 편리했기 때문입니다. 하지만 우리 프론트엔드 개발자들이 그 편리함 뒤에 있는 위험을 진지하게 마주한 것은 2024년 Polyfill.io 사건 이후였습니다. 수십만 개의 웹사이트가 아무 의심 없이 불러오던 스크립트가 악성 코드를 포함한 채 배포되기 시작했습니다.
이 글은 서드파티 스크립트 공급망 공격의 구조를 방어 관점에서 분석하고, SRI(Subresource Integrity) 해시 검증, CSP script-src 화이트리스트 설계, npm 의존성 감사, Lockfile 무결성 자동화, CI 파이프라인 통합까지 실무 적용 가능한 방어 체계를 단계적으로 정리합니다. 브라우저 보안 레이어 전반에 대해서는 브라우저 보안 강화 실전: CSP, Trusted Types, Nonce 기반 스크립트로 XSS 줄이기를 함께 읽으시길 권합니다.
1. 공급망 공격이란 무엇인가: Polyfill.io 사건이 가르쳐준 것
공급망 공격(supply chain attack)은 공격자가 최종 서비스를 직접 겨냥하지 않고, 그 서비스가 신뢰하는 외부 의존성을 오염시키는 방식입니다. 웹 생태계에서 이 공격면은 세 가지로 나뉩니다. 첫째는 CDN에서 제공하는 서드파티 스크립트, 둘째는 npm 레지스트리를 통해 설치하는 패키지, 셋째는 빌드 도구나 CI 환경의 플러그인입니다.
2024년 Polyfill.io 사건은 첫 번째 유형의 전형적인 사례입니다. 해당 도메인의 소유권이 중국 기업에 넘어간 이후, cdn.polyfill.io에서 제공하는 자바스크립트 번들에 모바일 환경에서만 활성화되는 악성 리디렉션 코드가 삽입됐습니다. 문제는 대부분의 사이트가 <script src="https://cdn.polyfill.io/v3/polyfill.min.js"> 한 줄만으로 외부 스크립트를 무조건 신뢰하고 있었다는 것입니다.
npm 생태계에서는 event-stream 패키지 오염 사건(2018년)이 대표적입니다. 2021년에는 ua-parser-js, coa, rc 패키지가 동시에 오염되는 사건도 있었습니다.
공통점은 하나입니다. 공격자는 신뢰 관계를 이용합니다. 방어의 시작은 "외부에서 오는 것은 모두 의심한다"는 원칙입니다.
2. SRI(Subresource Integrity) 작동 원리: 해시 기반 무결성 검증
SRI는 브라우저가 외부에서 로드하는 리소스의 내용이 예상한 것과 같은지 암호화 해시로 검증하는 메커니즘입니다. W3C SRI 스펙은 2016년에 권고 상태가 됐으며, 현재 모든 주요 브라우저가 지원합니다.
MDN 문서에 따르면 SRI는 <script>, <link> 요소에 integrity 속성을 추가하는 방식으로 동작합니다. 브라우저는 리소스를 다운로드한 뒤, 파일의 내용을 지정된 알고리즘으로 해시하고 integrity 값과 비교합니다. 두 값이 일치하지 않으면 브라우저는 해당 리소스의 실행을 거부합니다.
지원하는 해시 알고리즘은 SHA-256, SHA-384, SHA-512 세 가지입니다. 2026년 기준 실무 권장은 SHA-384 또는 SHA-512입니다.
SRI가 동작하려면 crossorigin 속성도 필수입니다. CDN이 CORS를 지원하지 않으면 SRI를 적용할 수 없습니다.
SRI의 한계도 명확합니다. 파일 내용이 바뀔 때마다 해시가 달라집니다. 따라서 버전이 명시된 고정 URL에만 적용할 수 있습니다.
3. SRI 해시 생성과 <script integrity> 적용
# SHA-384 해시 생성 (Base64 인코딩 포함)
curl -s https://cdn.example.com/libs/payment-sdk@2.4.1/payment.min.js \
| openssl dgst -sha384 -binary \
| openssl base64 -A
# 결과 예시: sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
<link
rel="stylesheet"
href="https://cdn.example.com/libs/design-tokens@1.3.0/tokens.min.css"
integrity="sha384-abc123def456...생략...xyz789"
crossorigin="anonymous"
/>
<script
src="https://cdn.example.com/libs/payment-sdk@2.4.1/payment.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
defer
></script>
Next.js나 Vite 같은 빌드 도구를 사용한다면 빌드 단계에서 SRI 해시를 자동 삽입하는 플러그인을 활용할 수 있습니다. vite-plugin-sri나 Next.js의 experimental.sri 옵션은 빌드 아티팩트에 대해 자동으로 해시를 생성해 HTML에 주입합니다.
4. CSP script-src 화이트리스트 설계: 'self', nonce, strict-dynamic
SRI는 파일 내용의 무결성을 검증하지만, CSP는 어떤 출처의 스크립트를 실행할 수 있는지를 정의합니다.
실무에서 script-src 정책을 설계할 때 가장 먼저 피해야 할 것은 'unsafe-inline'과 지나치게 넓은 호스트 허용(https:)입니다.
# 1단계: report-only로 관찰
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com; report-uri /api/csp-report
# 2단계: nonce 기반 정책
Content-Security-Policy: \
default-src 'self'; \
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' \
https://cdn.example.com; \
object-src 'none'; \
base-uri 'self'; \
require-trusted-types-for 'script'; \
report-uri /api/csp-report
# 3단계: SRI 요구 추가
Content-Security-Policy: \
default-src 'self'; \
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' \
https://cdn.example.com; \
require-sri-for script style; \
object-src 'none'; \
report-uri /api/csp-report
'strict-dynamic'은 nonce가 부여된 스크립트가 동적으로 추가하는 자식 스크립트를 신뢰하도록 허용합니다.
5. npm audit과 Snyk으로 의존성 취약점 탐지
npm audit는 npm 레지스트리의 취약점 데이터베이스와 현재 package-lock.json을 대조해 알려진 취약점을 보고합니다.
npm audit
npm audit --json
npm audit --audit-level=high
npm audit fix
npm audit fix --force는 메이저 버전 업그레이드를 포함할 수 있어 breaking change가 발생할 수 있습니다.
| 기준 | npm audit | Snyk |
|---|---|---|
| 데이터베이스 | GitHub Advisory Database | Snyk 전용 DB |
| 악성 패키지 탐지 | 제한적 | 지원 |
| 자동 PR 생성 | 미지원 | 지원 |
| 라이선스 정책 검사 | 미지원 | 지원 |
| 무료 플랜 | 완전 무료 | 오픈소스 한정 |
6. Lockfile 무결성 검증 자동화
Lockfile은 의존성 공급망 공격의 마지막 방어선 중 하나입니다.
# npm ci: lockfile을 엄격히 따름
npm ci
# pnpm
pnpm install --frozen-lockfile
# yarn
yarn install --immutable
npm install은 package.json의 semver 범위 내에서 최신 버전을 설치하려 할 수 있습니다. npm ci는 lockfile에 기록된 정확한 버전만 설치하고, lockfile이 없거나 package.json과 불일치하면 즉시 실패합니다.
7. CI 파이프라인 통합 전략: PR 차단 vs 알림 모드
name: Security Audit
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * *'
jobs:
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: npm audit
run: npm audit --audit-level=high --json > audit-report.json || true
- name: Parse audit results
id: audit
run: |
CRITICAL=$(cat audit-report.json | jq '.metadata.vulnerabilities.critical // 0')
HIGH=$(cat audit-report.json | jq '.metadata.vulnerabilities.high // 0')
echo "critical=${CRITICAL}" >> $GITHUB_OUTPUT
echo "high=${HIGH}" >> $GITHUB_OUTPUT
- name: Fail on critical vulnerabilities
if: steps.audit.outputs.critical > 0
run: exit 1
- name: SRI hash verification for known CDN resources
run: node scripts/verify-sri-hashes.js
critical 취약점은 항상 PR을 차단하고, high는 알림만 보내는 혼합 전략이 실용적입니다.
8. 인시던트 시뮬레이션: 오염된 CDN 라이브러리 탐지하기
탐지 레이어 1: SRI 해시 불일치 — 브라우저가 즉시 차단하고 CSP 위반 리포트 전송.
탐지 레이어 2: CSP 위반 리포트 급증 — 위반 리포트가 갑자기 증가하면 알림.
탐지 레이어 3: CI 검증 스크립트
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const sriManifest = JSON.parse(readFileSync('./sri-hashes.json', 'utf8'));
async function verifySriHashes(manifest) {
const violations = [];
for (const [url, expectedHash] of Object.entries(manifest)) {
const [algorithm, expected] = expectedHash.split('-');
const response = await fetch(url);
if (!response.ok) continue;
const buffer = await response.arrayBuffer();
const computed = createHash(algorithm)
.update(Buffer.from(buffer))
.digest('base64');
const hashString = `${algorithm}-${computed}`;
if (hashString !== expectedHash) {
console.error(`[SRI VIOLATION] ${url}`);
violations.push({ url, expected: expectedHash, computed: hashString });
}
}
if (violations.length > 0) {
console.error(`\n${violations.length}개의 SRI 해시 불일치 발견`);
process.exit(1);
}
}
verifySriHashes(sriManifest);
JWT 클레임 검증 누락과 마찬가지로, 공급망 공격도 검증 로직의 부재가 핵심 취약점입니다.
9. 실전 운영 체크리스트
SRI 적용 현황: 모든 외부 CDN 스크립트와 스타일시트에 integrity 속성이 있는지 확인합니다.
CSP 정책 유효성: 보안 헤더 분석 도구로 현재 CSP를 정기적으로 평가합니다.
npm 의존성 상태: npm audit 또는 Snyk으로 현재 critical과 high 취약점 수를 확인합니다. OWASP 서드파티 자바스크립트 관리 치트시트를 참고합니다.
Lockfile 관리: npm ci가 CI 파이프라인에서 사용되는지 확인합니다.
인시던트 대응 준비: 오염된 CDN 리소스가 발견됐을 때의 대응 절차를 문서화합니다.
결론
- SRI 해시를 모든 외부 CDN 리소스에 적용하고, sri-hashes.json을 버전 관리에 포함합니다.
- CSP
script-src에'unsafe-inline'없이 nonce 기반 정책과 허용 출처 최소화를 유지합니다. - CI에서
npm ci로 Lockfile을 엄격히 따르고,npm audit --audit-level=high로critical취약점은 PR을 차단합니다. - 스케줄 기반 의존성 감사로 신규 취약점을 놓치지 않습니다.
- 오염 탐지 시 즉시 해당 스크립트를 제거하는 긴급 배포 절차를 준비합니다.
SRI와 CSP, npm audit은 함께 동작할 때 공급망 공격의 영향 범위를 의미 있게 줄입니다.