콘텐츠 보안 정책(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 방어를 무력화하므로 피해야 합니다. 대신 세 가지 도구를 씁니다.
- nonce: 매 응답마다 서버가 생성한 임의의 일회용 값을 헤더(
script-src 'nonce-a1b2c3')와<script nonce="a1b2c3">양쪽에 붙입니다. 값이 일치하는 스크립트만 실행됩니다. 공격자는 매번 바뀌는 nonce를 예측할 수 없습니다. - hash: 인라인 스크립트 내용의 해시를 미리 계산해(
script-src 'sha256-...') 허용합니다. 스크립트가 정적이라 서버가 동적 nonce를 심기 어려울 때 유용합니다. - strict-dynamic: nonce/hash로 신뢰된 스크립트가 동적으로 로드하는 하위 스크립트까지 신뢰를 전파합니다. 호스트 화이트리스트(
https://cdn...)에 의존하지 않아, 화이트리스트 우회 공격에 강합니다.
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
흔한 깨짐과 특수 지시어
- 인라인 이벤트 핸들러(
onclick="...")와javascript:URL은 CSP에서 차단됩니다. 외부 스크립트의addEventListener로 옮겨야 합니다. - 인라인 스타일:
style-src에 nonce/hash 없이 인라인style속성을 쓰면 막힙니다.'unsafe-inline'을 피하려면 스타일을 파일로 분리하세요. frame-ancestors 'none': 이 페이지를 다른 사이트가<iframe>에 넣지 못하게 해 클릭재킹을 막습니다. 구식X-Frame-Options를 대체하는 지시어입니다.upgrade-insecure-requests: 페이지 안의http://요청을 자동으로https://로 승격시켜 혼합 콘텐츠(mixed content) 문제를 줄입니다.- 점진 강화: 완벽한 정책을 한 번에 만들려 하지 말고, report-only로 관측 → 리포트 기반 조정 → 강제 → nonce/strict-dynamic로 고도화의 순서를 밟는 것이 현실적입니다.