신뢰할 수 없는 출처의 웹 기능 포함

Inclusion of Web Functionality from Untrusted Source

설명

외부 CDN 등 신뢰할 수 없는 도메인에서 스크립트·스타일·모듈을 로드할 때 발생합니다. 로드된 코드가 브라우저에서 동일 출처 권한으로 실행되므로, 공급망이 오염되면 악성 스크립트가 사용자 브라우저에서 그대로 동작합니다. 공격자는 도메인 소유권 변경, 서버 침해, DNS 변조 등을 통해 악성 코드를 배포할 수 있으며, 로드된 스크립트로 쿠키·토큰 탈취, DOM 변조, 키로깅, 피싱 유도 등을 수행할 수 있습니다. 2024년에 polyfill.io 도메인에서 실제로 악성 스크립트가 배포된 사례가 보고되었습니다.

잠재적 영향

  • 악성 스크립트 실행: 외부에서 내려온 코드가 1st-party 컨텍스트에서 실행되어 해당 페이지에 허용된 권한을 악용할 수 있음.
  • 세션/토큰 탈취: document.cookie, localStorage, OAuth 토큰 등을 탈취하여 계정 장악 가능.
  • 데이터 유출: 입력 폼, 결제 정보, 개인식별정보(PII) 등 민감 데이터가 외부로 전송될 수 있음.
  • 콘텐츠 변조/피싱 유도: DOM 조작을 통해 가짜 로그인 창, 결제창을 띄워 사용자를 속일 수 있음.
  • 공급망 오염 확산: 한 번의 오염으로 해당 스크립트를 쓰는 페이지와 사용자에게 피해가 확산될 수 있음.

해결 방법

  • polyfill.io 등 신뢰할 수 없는 도메인에서 스크립트/리소스를 로드하지 마세요.
  • 가능하면 자체 호스팅(self-host)으로 필요한 polyfill/라이브러리를 고정 버전으로 배포하세요.
  • 외부 CDN이 불가피하다면 신뢰 가능한 CDN으로 교체하고, 정확한 버전 핀(pin)과 SRI(Subresource Integrity, integrity + crossorigin) 값을 사용하세요.
  • 동적 로딩(import(), createElement, fetch + eval 등)을 사용하는 경우, 허용 도메인(allowlist)을 코드/빌드 설정에 고정하고 런타임 변경을 제한하세요.
  • CSP(Content-Security-Policy)로 script-src를 'self' 및 엄격한 allowlist로 제한하고, 가능하면 'strict-dynamic', 'nonce' 또는 'sha256-...' 해시를 사용하세요.
  • 레거시 브라우저 지원이 정말 필요한지 재검토하고, 폴리필은 기능 감지(Feature Detection) 기반으로 최소한만 로드하세요.
  • 제3자 리소스 변경 감시(무결성 검사, 모니터링)와 정기적 보안 검토를 수행하세요.

예시

변경 전

javascript
// server.js (Node/Express) - 취약한 예시
import express from 'express';
const app = express();

// 환경변수로 바꿀 수 있게 했지만 기본값이 polyfill.io
// 운영 중 공급망이 오염되면 모든 사용자가 악성 스크립트를 받게 됩니다.
app.get('/', (req, res) => {
  const polyfillUrl = process.env.POLYFILL_URL || 'https://polyfill.io/v3/polyfill.min.js?features=default';
  res.send(`<!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>App</title>
      <script src="${polyfillUrl}"></script>
    </head>
    <body>
      <h1>Hello</h1>
    </body>
  </html>`);
});

app.listen(3000);

변경 후

javascript
// server.js (Node/Express) - 안전한 예시
import express from 'express';
const app = express();

// 1) 자체 호스팅: 빌드 시 검증된 고정 버전 파일을 /assets 에 배포
app.use('/assets', express.static('public/assets', { immutable: true, maxAge: '1y' }));

app.get('/', (req, res) => {
  // 외부 도메인에 의존하지 않고, 검증된 파일을 고정 경로로 제공
  // (외부 CDN을 쓸 경우: 정확한 버전 핀 + integrity + crossorigin 적용)
  res.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'");
  res.send(`<!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>App</title>
      <script src="/assets/polyfills-4.8.0.min.js" defer></script>
    </head>
    <body>
      <h1>Hello</h1>
    </body>
  </html>`);
});

app.listen(3000);

설명:

  • 변경 전: 외부 polyfill.io 도메인에서 스크립트를 직접 로드합니다. 해당 도메인이 소유권 변경·침해·DNS 변조 등으로 오염되면 악성 JS가 동일 출처 권한으로 실행되어 쿠키/토큰 탈취, DOM 변조, 피싱 유도 등이 가능합니다. 또한 URL을 환경변수로 바꿀 수 있게 해 런타임에 임의 도메인으로 교체될 위험도 있습니다.
  • 변경 후: 검증된 파일을 자체 호스팅하고 고정 경로로 제공하여 제3자 도메인 의존을 제거했습니다. CSP로 script-src를 'self'로 제한해 외부 스크립트 로드를 차단합니다. 외부 CDN을 써야 한다면 버전 핀과 SRI를 적용해 신뢰한 원본과 파일이 일치하는지 검증하고, 허용 도메인만 명시해 동적 변경을 막아 공급망 공격 가능성을 크게 줄입니다.

참조