엄격한 콘텐츠 보안 정책 (CSP)으로 교차 사이트 스크립팅 (XSS) 완화

교차 사이트 스크립팅에 대한 심층 방어를 위해 스크립트 nonce 또는 해시를 기반으로 CSP를 배포하는 방법

루카스 바이첼바움
루카스 바이첼바움

엄격한 콘텐츠 보안 정책 (CSP)을 배포해야 하는 이유는 무엇인가요?

악성 스크립트를 웹 애플리케이션에 삽입하는 기능인 교차 사이트 스크립팅 (XSS)은 10년 넘게 가장 큰 웹 보안 취약점 중 하나였습니다.

콘텐츠 보안 정책 (CSP)은 XSS를 완화하는 데 도움이 되는 보안 강화 레이어입니다. CSP를 구성하려면 웹페이지에 콘텐츠 보안 정책 HTTP 헤더를 추가하고 사용자 에이전트가 해당 페이지에 로드할 수 있는 리소스를 제어하는 값을 설정해야 합니다. 이 도움말에서는 일반적으로 사용되는 호스트 허용 목록 기반 CSP 대신 nonce 또는 해시를 기반으로 CSP를 사용하여 XSS를 완화하는 방법을 설명합니다. 이러한 CSP는 종종 페이지를 XSS에 노출시킵니다. 대부분의 구성에서 우회될 수 있기 때문입니다.

nonce 또는 해시를 기반으로 하는 콘텐츠 보안 정책을 엄격한 CSP라고 합니다. 애플리케이션이 엄격한 CSP를 사용하는 경우, HTML 삽입의 결함을 발견한 공격자는 일반적으로 이를 사용하여 브라우저가 취약한 문서의 맥락에서 악성 스크립트를 실행하도록 강요할 수 없습니다. 이는 엄격한 CSP에서 서버에서 생성된 올바른 nonce 값이 있는 해싱된 스크립트 또는 스크립트만 허용하므로 공격자가 주어진 응답에 대한 올바른 nonce를 모르면 스크립트를 실행할 수 없기 때문입니다.

브라우저 호환성

엄격한 CSP는 모든 최신 브라우저 엔진에서 지원됩니다.

브라우저 지원

  • 52
  • 79
  • 52
  • 15.4

소스

사이트에 이미 script-src www.googleapis.com와 같은 CSP가 있는 경우 교차 사이트 스크립팅에 효과적이지 않을 수 있습니다. 이 유형의 CSP를 허용 목록 CSP라고 하며 다음과 같은 몇 가지 단점이 있습니다.

이로 인해 허용 목록 CSP는 일반적으로 공격자가 XSS를 악용하는 것을 방지하는 데 비효율적입니다. 따라서 위에서 설명한 함정을 피할 수 있도록 암호화 임시값 또는 해시를 기반으로 하는 엄격한 CSP를 사용하는 것이 좋습니다.

허용 목록 CSP
  • 사이트를 효과적으로 보호하지 못합니다. ❌
  • 고도로 맞춤설정해야 합니다. 😓
엄격한 CSP
  • 사이트를 효과적으로 보호합니다. ✅
  • 항상 동일한 구조를 가집니다. 😌

엄격한 콘텐츠 보안 정책이란 무엇인가요?

엄격한 콘텐츠 보안 정책은 다음과 같은 구조로 되어 있으며, 다음 HTTP 응답 헤더 중 하나를 설정하여 사용 설정됩니다.

  • nonce 기반의 엄격한 CSP
Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

  • 해시 기반 엄격한 CSP
Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

다음 속성은 위와 같은 CSP를 '엄격'하므로 안전합니다.

  • nonce 'nonce-{RANDOM}' 또는 해시 'sha256-{HASHED_INLINE_SCRIPT}'를 사용하여 어떤 <script> 태그가 사이트 개발자가 신뢰하며 사용자의 브라우저에서 실행되도록 허용해야 하는지를 나타냅니다.
  • 이미 신뢰할 수 있는 스크립트에서 생성된 스크립트의 실행을 자동으로 허용하여 nonce 또는 해시 기반 CSP를 배포하는 노력을 줄이도록 'strict-dynamic'를 설정합니다. 이렇게 하면 대부분의 서드 파티 JavaScript 라이브러리 및 위젯 사용도 차단 해제됩니다.
  • URL 허용 목록을 기반으로 하지 않으므로 일반적인 CSP 우회가 발생하지 않습니다.
  • 인라인 이벤트 핸들러 또는 javascript: URI와 같이 신뢰할 수 없는 인라인 스크립트를 차단합니다.
  • Flash와 같은 위험한 플러그인을 사용 중지하도록 object-src를 제한합니다.
  • <base> 태그의 삽입을 차단하도록 base-uri를 제한합니다. 이렇게 하면 공격자가 상대 URL에서 로드된 스크립트의 위치를 변경할 수 없습니다.

엄격한 CSP 채택

엄격한 CSP를 채택하려면 다음을 수행해야 합니다.

  1. 애플리케이션에서 nonce 기반 또는 해시 기반 CSP를 설정해야 하는지 결정합니다.
  2. 엄격한 콘텐츠 보안 정책이란 무엇인가요? 섹션에서 CSP를 복사하여 애플리케이션에서 응답 헤더로 설정합니다.
  3. HTML 템플릿 및 클라이언트 측 코드를 리팩터링하여 CSP와 호환되지 않는 패턴을 삭제합니다.
  4. CSP를 배포합니다.

이 프로세스 전반에 걸쳐 Lighthouse (v7.3.0 이상, --preset=experimental 플래그가 있음) 권장사항 감사를 사용하여 사이트에 CSP가 있는지, 그리고 XSS에 효과적일 만큼 엄격한지 확인할 수 있습니다.

시행 모드에서 CSP를 찾을 수 없다는 Lighthouse 보고서 경고입니다.

1단계: nonce 또는 해시 기반 CSP가 필요한지 결정

엄격한 CSP에는 nonce 기반과 해시 기반이라는 두 가지 유형이 있습니다. 기준 프로필의 작동 방식은 다음과 같습니다.

  • nonce 기반 CSP: 런타임 시 랜덤 숫자를 생성하여 CSP에 포함한 후 페이지의 모든 스크립트 태그와 연결합니다. 공격자는 페이지에 악성 스크립트를 포함하거나 실행할 수 없습니다. 해당 스크립트의 올바른 랜덤 숫자를 추측해야 하기 때문입니다. 이는 숫자를 추측할 수 없고 모든 응답에 대해 런타임 시 새로 생성된 경우에만 작동합니다.
  • 해시 기반 CSP: 모든 인라인 스크립트 태그의 해시가 CSP에 추가됩니다. 스크립트마다 해시가 다릅니다. 공격자는 페이지에 악성 스크립트를 포함하거나 실행할 수 없습니다. 해당 스크립트의 해시가 CSP에 존재해야 하기 때문입니다.

엄격한 CSP 접근 방식을 선택하기 위한 기준:

엄격한 CSP 접근 방식을 선택하기 위한 기준
nonce 기반 CSP 서버에서 렌더링된 HTML 페이지의 경우, 응답마다 새로운 임의의 토큰 (nonce)을 만들 수 있습니다.
해시 기반 CSP 정적으로 게재되는 HTML 페이지 또는 캐시해야 하는 HTML 페이지 예를 들어 Angular, React 등의 프레임워크로 빌드한 단일 페이지 웹 애플리케이션으로, 서버 측 렌더링 없이 정적으로 제공됩니다.

2단계: 엄격한 CSP 설정 및 스크립트 준비

CSP를 설정할 때 몇 가지 옵션을 사용할 수 있습니다.

  • 보고서 전용 모드 (Content-Security-Policy-Report-Only) 또는 시행 모드 (Content-Security-Policy)입니다. 보고서 전용에서는 CSP가 아직 리소스를 차단하지 않으며 어떠한 중단도 발생하지 않지만, 차단된 항목에 관한 오류를 확인하고 보고서를 받을 수 있습니다. 로컬에서는 CSP를 설정하는 중인 경우에는 별로 중요하지 않습니다. 두 모드 모두 브라우저 콘솔에서 오류를 표시하기 때문입니다. 혹시라도, 시행 모드를 사용하면 더 쉽게 차단된 리소스를 확인하고 CSP를 조정할 수 있습니다. 페이지가 손상된 것처럼 보이게 하기 때문입니다. 보고서 전용 모드는 프로세스 후반부에서 가장 유용합니다 (5단계 참고).
  • 헤더 또는 HTML <meta> 태그입니다. 로컬 개발의 경우 <meta> 태그를 사용하면 CSP를 조정하고 사이트에 미치는 영향을 빠르게 확인할 수 있습니다. 하지만 다음과 같은 상황이 발생할 수 있습니다.
    • 나중에 프로덕션에서 CSP를 배포할 때 HTTP 헤더로 설정하는 것이 좋습니다.
    • CSP를 보고서 전용 모드로 설정하려면 헤더로 설정해야 합니다. CSP 메타 태그는 보고서 전용 모드를 지원하지 않습니다.

옵션 A: nonce 기반 CSP

애플리케이션에 다음 Content-Security-Policy HTTP 응답 헤더를 설정합니다.

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

CSP용 nonce 생성

nonce는 페이지 로드당 한 번만 사용되는 랜덤 숫자입니다. nonce 기반 CSP는 공격자가 nonce 값을 추측할 수 없는 경우에만 XSS를 완화할 수 있습니다. CSP의 nonce는 다음과 같아야 합니다.

  • 암호학적으로 강력한 임의의 값 (길이가 128비트 이상인 것이 가장 좋음)
  • 모든 응답에 대해 새로 생성
  • Base64 인코딩

다음은 서버 측 프레임워크에서 CSP nonce를 추가하는 방법에 관한 몇 가지 예입니다.

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

<script> 요소에 nonce 속성 추가

nonce 기반 CSP의 경우 모든 <script> 요소에는 CSP 헤더에 지정된 임의의 nonce 값과 일치하는 nonce 속성이 있어야 합니다 (모든 스크립트가 동일한 nonce를 가질 수 있음). 첫 번째 단계는 이러한 속성을 모든 스크립트에 추가하는 것입니다.

CSP에서 차단함
<script src="/path/to/script.js"></script>
<script>foo()</script>
nonce 속성이 없으므로 CSP에서 이러한 스크립트를 차단합니다.

옵션 B: 해시 기반 CSP 응답 헤더

애플리케이션에 다음 Content-Security-Policy HTTP 응답 헤더를 설정합니다.

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

일부 인라인 스크립트의 경우 문법은 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'입니다.

동적으로 소스 스크립트 로드

외부에서 가져온 모든 스크립트는 인라인 스크립트를 통해 동적으로 로드해야 합니다. CSP 해시는 인라인 스크립트에 대해서만 브라우저에서 지원되기 때문입니다. 소스 스크립트의 해시는 여러 브라우저에서 잘 지원되지 않습니다.

CSP에서 차단함
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
인라인 스크립트만 해싱할 수 있으므로 CSP가 이러한 스크립트를 차단합니다.
CSP에서 허용함
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
이 스크립트를 실행하려면 인라인 스크립트의 해시를 계산하여 CSP 응답 헤더에 추가하여 {HASHED_INLINE_SCRIPT} 자리표시자를 대체해야 합니다. 해시의 양을 줄이려면 모든 인라인 스크립트를 단일 스크립트로 병합해도 됩니다. 실제 동작을 보려면 예시를 확인하고 코드를 살펴보세요.

스크립트 로드 고려사항

위 코드 스니펫에는 막대가 먼저 로드되더라도 foo가 bar 전에 실행되도록 s.async = false가 추가됩니다. 이 스니펫에서 s.async = false는 스크립트가 로드되는 동안 파서를 차단하지 않습니다. 스크립트가 동적으로 추가되기 때문입니다. 파서는 async 스크립트에서 작동하는 것처럼 스크립트가 실행되는 동안에만 중지됩니다. 그러나 이 스니펫에서는 다음 사항에 유의하세요.

  • 문서 다운로드가 완료되기 전에 두 스크립트 중 하나 또는 모두가 실행될 수 있습니다. 스크립트가 실행될 때까지 문서가 준비되도록 하려면 DOMContentLoaded 이벤트가 발생할 때까지 기다린 후에 스크립트를 추가해야 합니다. 이로 인해 스크립트가 충분히 일찍 다운로드되지 않아서 성능 문제가 발생하는 경우 페이지 앞부분에서 미리 로드된 태그를 사용할 수 있습니다.
  • defer = true에서 아무것도 하지 않습니다. 이 동작이 필요하면 실행 시점에 스크립트를 수동으로 실행해야 합니다.

3단계: HTML 템플릿 및 클라이언트 측 코드를 리팩터링하여 CSP와 호환되지 않는 패턴 삭제

인라인 이벤트 핸들러 (예: onclick="…", onerror="…") 및 JavaScript URI (<a href="javascript:…">)를 사용하여 스크립트를 실행할 수 있습니다. 즉, XSS 버그를 발견한 공격자가 이러한 종류의 HTML을 삽입하여 악성 JavaScript를 실행할 수 있습니다. nonce 또는 해시 기반 CSP에서는 이러한 마크업을 사용할 수 없습니다. 사이트에서 위에 설명된 패턴을 사용하는 경우 더 안전한 대안으로 리팩터링해야 합니다.

이전 단계에서 CSP를 사용 설정한 경우 CSP가 호환되지 않는 패턴을 차단할 때마다 콘솔에서 CSP 위반을 확인할 수 있습니다.

Chrome 개발자 콘솔의 CSP 위반 신고

대부분의 경우 해결 방법은 간단합니다.

인라인 이벤트 핸들러를 리팩터링하려면 자바스크립트 블록에서 추가되도록 다시 작성하세요.

CSP에서 차단함
<span onclick="doThings();">A thing.</span>
CSP에서 인라인 이벤트 핸들러를 차단합니다.
CSP에서 허용함
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
CSP에서 자바스크립트를 통해 등록되는 이벤트 핸들러를 허용합니다.

javascript: URI의 경우 비슷한 패턴을 사용할 수 있습니다.

CSP에서 차단함
<a href="javascript:linkClicked()">foo</a>
CSP에서 javascript: URI를 차단합니다.
CSP에서 허용함
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
CSP에서 자바스크립트를 통해 등록되는 이벤트 핸들러를 허용합니다.

JavaScript에서 eval() 사용

애플리케이션에서 eval()를 사용하여 JSON 문자열 직렬화를 JS 객체로 변환하는 경우 이러한 인스턴스를 더 빠른 JSON.parse()로 리팩터링해야 합니다.

eval()의 모든 사용을 삭제할 수 없는 경우에도 엄격한 nonce 기반 CSP를 설정할 수 있지만 'unsafe-eval' CSP 키워드를 사용해야 하므로 정책의 보안이 약간 취약해집니다.

다음의 엄격한 CSP Codelab에서 이러한 리팩터링의 예와 더 많은 예를 확인할 수 있습니다.

4단계 (선택사항): 이전 브라우저 버전을 지원하도록 대체 기능 추가하기

브라우저 지원

  • 52
  • 79
  • 52
  • 15.4

소스

위에 나열된 버전보다 이전 버전의 브라우저를 지원해야 하는 경우 다음 단계를 따르세요.

  • 'strict-dynamic'를 사용하려면 이전 버전의 Safari에서 대체 수단으로 https:를 추가해야 합니다. 이렇게 하면 다음 작업이 가능합니다.
    • 'strict-dynamic'를 지원하는 모든 브라우저는 https: 대체를 무시하므로 정책의 강도가 약화되지는 않습니다.
    • 이전 브라우저에서는 외부에서 가져온 스크립트를 HTTPS 출처에서 가져온 경우에만 로드할 수 있습니다. 이는 엄격한 CSP보다 안전하지 않으며 대체 방식이지만 javascript: URI 삽입과 같은 일반적인 특정 XSS 원인을 여전히 방지합니다. 'unsafe-inline'가 해시 또는 nonce가 있을 때 존재하거나 무시되기 때문입니다.
  • 오래된 브라우저 버전 (4년 이상)과의 호환성을 위해 'unsafe-inline'를 대체 수단으로 추가할 수 있습니다. CSP nonce 또는 해시가 있는 경우 모든 최신 브라우저에서 'unsafe-inline'를 무시합니다.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

5단계: CSP 배포

로컬 개발 환경에서 CSP에 의해 차단되는 합법적인 스크립트가 없는지 확인한 후 CSP를 프로덕션 환경에 배포 (스테이징 후) 진행할 수 있습니다.

  1. (선택사항) Content-Security-Policy-Report-Only 헤더를 사용하여 CSP를 보고서 전용 모드로 배포합니다. Reporting API에 대해 자세히 알아보세요. 보고서 전용 모드는 실제로 CSP 제한을 적용하기 전에 프로덕션 환경에서 새 CSP와 같은 잠재적 브레이킹 체인지를 테스트하는 데 유용합니다. 보고서 전용 모드에서는 CSP가 애플리케이션의 동작에 영향을 미치지 않으며, 실제로 문제가 발생하지는 않습니다. 하지만 CSP와 호환되지 않는 패턴이 발생하는 경우 브라우저에서 콘솔 오류 및 위반 보고서를 생성합니다. 따라서 최종 사용자에게 어떤 문제가 발생했는지 확인할 수 있습니다.
  2. CSP가 최종 사용자의 서비스 중단을 일으키지 않는다고 확신한다면 Content-Security-Policy 응답 헤더를 사용하여 CSP를 배포합니다. 이 단계를 완료해야만 CSP가 XSS로부터 애플리케이션을 보호하기 시작합니다. 서버 측에서 HTTP 헤더를 통해 CSP를 설정하는 것이 <meta> 태그로 설정하는 것보다 안전합니다. 가능하면 헤더를 사용하세요.

제한사항

일반적으로 엄격한 CSP는 XSS 완화에 도움이 되는 강력한 보안 레이어를 제공합니다. 대부분의 경우 CSP는 공격 표면을 크게 줄입니다 (javascript: URI와 같은 위험한 패턴은 완전히 사용 중지됨). 그러나 사용 중인 CSP의 유형 ('strict-dynamic' 유무와 관계없음, nonce, 해시)에 따라 CSP가 보호하지 않는 경우도 있습니다.

  • 스크립트를 nonce이지만 본문 또는 해당 <script> 요소의 src 매개변수에 직접 삽입되는 경우
  • 인수 값에 따라 script DOM 노드를 생성하는 라이브러리 함수를 포함하여 동적으로 생성된 스크립트 (document.createElement('script'))의 위치에 삽입이 발생한 경우. 여기에는 jQuery의 .html() 및 jQuery < 3.0의 .get().post()와 같은 몇 가지 일반적인 API가 포함되어 있습니다.
  • 이전 AngularJS 애플리케이션에 템플릿 삽입이 있는 경우. AngularJS 템플릿을 삽입할 수 있는 공격자는 이 템플릿을 사용하여 임의의 JavaScript를 실행할 수 있습니다.
  • 정책에 'unsafe-eval'가 포함된 경우 eval(), setTimeout(), 기타 거의 사용되지 않는 몇 가지 API에 삽입됩니다.

개발자와 보안 엔지니어는 코드 검토 및 보안 감사 시 이러한 패턴에 특히 주의를 기울여야 합니다. 위에 설명된 케이스에 관한 자세한 내용은 이 CSP 프레젠테이션을 참고하세요.

추가 자료