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

Lukas Weichselbaum
Lukas Weichselbaum

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

교차 사이트 스크립팅 (XSS)은 웹 앱에 악성 스크립트를 삽입하는 기능으로, 10년 이상 가장 큰 웹 보안 취약점 중 하나였습니다.

콘텐츠 보안 정책 (CSP)은 XSS를 완화하는 데 도움이 되는 추가 보안 계층입니다. CSP를 구성하려면 웹페이지에 Content-Security-Policy HTTP 헤더를 추가하고 사용자 에이전트가 해당 페이지에 로드할 수 있는 리소스를 제어하는 값을 설정합니다.

이 페이지에서는 일반적으로 사용되는 호스트 허용 목록 기반 CSP가 대부분의 구성에서 우회될 수 있어 페이지가 XSS에 노출되는 경우가 많으므로, nonce 또는 해시 기반 CSP를 사용하여 XSS를 완화하는 방법을 설명합니다.

핵심 용어: nonce<script> 태그를 신뢰할 수 있는 것으로 표시하는 데 사용할 수 있는 한 번만 사용되는 난수입니다.

핵심 용어: 해시 함수는 입력 값을 해시라는 압축된 숫자 값으로 변환하는 수학 함수입니다. 해시(예: SHA-256)를 사용하여 인라인 <script> 태그를 신뢰할 수 있는 것으로 표시할 수 있습니다.

넌스 또는 해시에 기반한 콘텐츠 보안 정책을 흔히 엄격한 CSP라고 합니다. 애플리케이션이 엄격한 CSP를 사용하는 경우 HTML 삽입 결함을 발견한 공격자는 이를 사용하여 브라우저가 취약한 문서에서 악성 스크립트를 실행하도록 강제할 수 없습니다. 이는 엄격한 CSP에서 해시된 스크립트 또는 서버에서 생성된 올바른 nonce 값이 있는 스크립트만 허용하기 때문입니다. 따라서 공격자는 특정 응답의 올바른 nonce를 알지 못하면 스크립트를 실행할 수 없습니다.

엄격한 CSP를 사용해야 하는 이유

사이트에 이미 script-src www.googleapis.com와 같은 CSP가 있는 경우 크로스 사이트에 효과적이지 않을 수 있습니다. 이러한 유형의 CSP를 허용 목록 CSP라고 합니다. 맞춤설정이 많이 필요하며 공격자가 우회할 수 있습니다.

암호화된 nonce 또는 해시에 기반한 엄격한 CSP는 이러한 문제를 방지합니다.

엄격한 CSP 구조

기본적인 엄격한 콘텐츠 보안 정책은 다음 HTTP 응답 헤더 중 하나를 사용합니다.

Nonce 기반 엄격한 CSP

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none&#39;;
  base-uri 'none';
논스 기반 엄격한 CSP의 작동 방식

해시 기반 엄격한 CSP

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

다음 속성은 이와 같은 CSP를 '엄격'하게 만들어 안전하게 합니다.

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

엄격한 CSP 채택

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

  1. 애플리케이션이 nonce 또는 해시 기반 CSP를 설정해야 하는지 결정합니다.
  2. 엄격한 CSP 구조 섹션에서 CSP를 복사하고 애플리케이션 전체에서 응답 헤더로 설정합니다.
  3. CSP와 호환되지 않는 패턴을 삭제하도록 HTML 템플릿과 클라이언트 측 코드를 리팩터링합니다.
  4. CSP를 배포합니다.

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

시행 모드에서 CSP를 찾을 수 없다는 Lighthouse 보고서 경고
사이트에 CSP가 없으면 Lighthouse에 이 경고가 표시됩니다.

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

두 가지 유형의 엄격한 CSP 작동 방식은 다음과 같습니다.

Nonce 기반 CSP

nonce 기반 CSP를 사용하면 런타임에 난수를 생성하고 이를 CSP에 포함하며 페이지의 모든 스크립트 태그와 연결합니다. 공격자는 페이지에 악성 스크립트를 포함하거나 실행할 수 없습니다. 스크립트의 올바른 난수를 추측해야 하기 때문입니다. 이는 숫자를 추측할 수 없고 런타임에 모든 응답에 대해 새로 생성되는 경우에만 작동합니다.

서버에서 렌더링된 HTML 페이지에 nonce 기반 CSP를 사용합니다. 이러한 페이지의 경우 모든 응답에 대해 새로운 난수를 만들 수 있습니다.

해시 기반 CSP

해시 기반 CSP의 경우 모든 인라인 스크립트 태그의 해시가 CSP에 추가됩니다. 스크립트마다 해시가 다릅니다. 공격자는 페이지에 악성 스크립트를 포함하거나 실행할 수 없습니다. 스크립트가 실행되려면 스크립트의 해시가 CSP에 있어야 하기 때문입니다.

정적으로 제공되는 HTML 페이지 또는 캐시해야 하는 페이지에는 해시 기반 CSP를 사용합니다. 예를 들어 서버 측 렌더링 없이 정적으로 제공되는 Angular, React 등의 프레임워크로 빌드된 단일 페이지 웹 애플리케이션에 해시 기반 CSP를 사용할 수 있습니다.

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 값을 추측할 수 없는 경우에만 nonce 기반 CSP가 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(&>quot;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에서 허용하도록 모든 스크립트에 이러한 속성을 추가하는 것입니다.

옵션 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에 의해 허용됨
<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.hea<d.appen>dChild(s);
  });
/script
이 스크립트를 실행하려면 인라인 스크립트의 해시를 계산하여 CSP 응답 헤더에 추가하고 {HASHED_INLINE_SCRIPT} 자리표시자를 대체해야 합니다. 해시 수를 줄이려면 모든 인라인 스크립트를 단일 스크립트로 병합하면 됩니다. 실제로 작동하는 모습을 보려면 이 코드를 참고하세요.
CSP에 의해 차단됨
<script src="https://example.org/fo><o.js&qu>o<t;/script
script src="https://exam><ple.org>/bar.js"/script
이러한 스크립트는 동적으로 추가되지 않았으며 허용된 소스와 일치하는 integrity 속성이 없으므로 CSP에 의해 차단됩니다.

스크립트 로드 고려사항

인라인 스크립트 예시에서는 bar가 먼저 로드되더라도 foobar 전에 실행되도록 s.async = false를 추가합니다. 이 스니펫에서는 스크립트가 동적으로 추가되므로 스크립트가 로드되는 동안 s.async = false가 파서를 차단하지 않습니다. 파서는 async 스크립트와 마찬가지로 스크립트가 실행되는 동안에만 중지됩니다. 하지만 이 스니펫을 사용할 때는 다음 사항에 유의하세요.

  • 문서 다운로드가 완료되기 전에 스크립트가 하나 또는 둘 다 실행될 수 있습니다. 스크립트가 실행될 때까지 문서가 준비되도록 하려면 스크립트를 추가하기 전에 DOMContentLoaded 이벤트를 기다립니다. 스크립트가 충분히 일찍 다운로드되지 않아 성능 문제가 발생하는 경우 페이지에서 더 일찍 프리로드 태그를 사용하세요.
  • defer = true는 아무것도 하지 않습니다. 이 동작이 필요한 경우 필요할 때 스크립트를 수동으로 실행하세요.

3단계: HTML 템플릿 및 클라이언트 측 코드 리팩터링

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

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

Chrome 개발자 콘솔의 CSP 위반 보고서
차단된 코드의 콘솔 오류

대부분의 경우 수정은 간단합니다.

인라인 이벤트 핸들러 리팩터링

CSP에 의해 허용됨
<span id="th>ings&quo<t;A t>h<ing./span
script nonce=>"${nonce}"
  document.getElementById('things').addEventL<istener>('click', doThings);
/script
CSP는 JavaScript를 사용하여 등록된 이벤트 핸들러를 허용합니다.
CSP에 의해 차단됨
<span onclick="doThing>s();&quo<t;A t>hing./span
CSP는 인라인 이벤트 핸들러를 차단합니다.

javascript: URI 리팩터링

CSP에 의해 허용됨
<a id=">;fo<o&>q<uot;foo/a
script nonce=>"${nonce}"
  document.getElementById('foo').addEventList<ener(&#>39;click', linkClicked);
/script
CSP는 JavaScript를 사용하여 등록된 이벤트 핸들러를 허용합니다.
CSP에 의해 차단됨
<a href="javascript:linkClick>ed(<)&>quot;foo/a
CSP가 JavaScript URI를 차단합니다.

JavaScript에서 eval() 삭제

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

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

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

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

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

이전 브라우저 버전을 지원해야 하는 경우:

  • strict-dynamic를 사용하려면 이전 버전의 Safari의 대체로 https:을 추가해야 합니다. 이 경우 다음 사항이 적용됩니다.
    • strict-dynamic를 지원하는 모든 브라우저는 https: 대체를 무시하므로 정책의 강도가 줄어들지 않습니다.
    • 이전 브라우저에서는 외부에서 가져온 스크립트가 HTTPS 출처에서 가져온 경우에만 로드될 수 있습니다. 이는 엄격한 CSP보다 안전하지는 않지만 javascript: URI 삽입과 같은 일반적인 XSS 원인을 방지합니다.
  • 매우 오래된 브라우저 버전 (4년 이상)과의 호환성을 보장하려면 unsafe-inline을 대체로 추가하면 됩니다. 최근 브라우저는 CSP nonce 또는 해시가 있는 경우 unsafe-inline를 무시합니다.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 9;none';
  base-uri 'none';

5단계: CSP 배포

CSP가 로컬 개발 환경에서 적법한 스크립트를 차단하지 않는지 확인한 후 CSP를 스테이징 환경에 배포한 다음 프로덕션 환경에 배포할 수 있습니다.

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

제한사항

엄격한 CSP는 일반적으로 XSS를 완화하는 데 도움이 되는 강력한 추가 보안 계층을 제공합니다. 대부분의 경우 CSP는 javascript: URI와 같은 위험한 패턴을 거부하여 공격에 노출되는 영역을 크게 줄입니다. 하지만 사용 중인 CSP 유형 (nonce, 해시, 'strict-dynamic' 사용 여부)에 따라 CSP가 앱을 제대로 보호하지 못하는 경우가 있습니다.

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

개발자와 보안 엔지니어는 코드 검토 및 보안 감사 중에 이러한 패턴에 특히 주의해야 합니다. 이러한 사례에 대한 자세한 내용은 콘텐츠 보안 정책: 강화와 완화 사이의 성공적인 혼란을 참고하세요.

추가 자료