실전 콘텐츠 보안 정책 (CSP)

정책 만들기, nonce vs hash vs strict-dynamic, report-only 점진 도입, 흔한 깨짐, 리포팅 엔드포인트, default-src 폴백, frame-ancestors, upgrade-insecure-requests를 실무 관점으로 다룹니다.

콘텐츠 보안 정책(CSP, Content Security Policy)은 브라우저에게 '이 페이지에서 어떤 출처의 리소스만 허용할지'를 선언하는 응답 헤더입니다. 주 목적은 크로스 사이트 스크립팅(XSS) 완화로, 공격자가 페이지에 주입한 악성 스크립트가 실행되거나 데이터를 탈취하는 것을 막습니다. Content-Security-Policy 헤더에 지시어(directive)들을 세미콜론으로 나열해 정책을 구성합니다.

정책 만들기: 지시어와 default-src

정책은 리소스 종류별 지시어로 이뤄집니다. script-src(스크립트), style-src(스타일), img-src(이미지), connect-src(fetch·XHR·WebSocket) 등입니다. 이들 중 명시되지 않은 종류는 default-src 가 폴백으로 적용됩니다. 즉 default-src를 촘촘히 걸어 두고, 특정 종류만 예외로 열어 주는 식으로 설계합니다.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  upgrade-insecure-requests

nonce vs hash vs strict-dynamic

가장 어려운 부분은 인라인 스크립트를 안전하게 허용하는 것입니다. 'unsafe-inline'은 모든 인라인 스크립트를 허용해 사실상 XSS 방어를 무력화하므로 피해야 합니다. 대신 세 가지 도구를 씁니다.

Content-Security-Policy:
  script-src 'nonce-a1b2c3' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none'

위 예에서 strict-dynamic을 지원하는 최신 브라우저는 nonce만 신뢰하고 https:·'unsafe-inline'을 무시하며, 이를 모르는 구형 브라우저는 그 폴백을 씁니다. 이렇게 하위 호환을 확보합니다.

report-only로 점진 도입하기 — 기존 사이트에 CSP를 바로 강제하면 정상 리소스까지 차단되어 화면이 깨지기 쉽습니다. 그래서 Content-Security-Policy-Report-Only 헤더로 먼저 배포합니다. 이 모드는 정책을 강제하지 않고 위반만 리포팅하므로, 실제 사용자 트래픽에서 무엇이 차단될지 데이터를 모은 뒤 정책을 다듬어 안전하게 강제 모드로 전환할 수 있습니다.

리포팅 엔드포인트 — 위반 보고를 받으려면 수집 경로를 지정합니다. 예전에는 report-uri를 썼고, 최신 방식은 Reporting-Endpoints 헤더로 이름 있는 엔드포인트를 정의한 뒤 CSP에서 report-to로 가리킵니다. 브라우저는 위반이 생기면 차단된 리소스·위반한 지시어 등을 담은 JSON을 그 엔드포인트로 POST합니다.

Reporting-Endpoints: csp-reports="https://example.com/csp-reports"
Content-Security-Policy-Report-Only:
  default-src 'self'; report-to csp-reports

흔한 깨짐과 특수 지시어