ToolPal
노트북 위의 자물쇠와 보안 개념

CSP 생성기: Content Security Policy, 이제 무섭지 않다

📷 Pixabay / Pexels

CSP 생성기: Content Security Policy, 이제 무섭지 않다

Content Security Policy(CSP)는 XSS 공격을 막는 강력한 HTTP 헤더지만, 직접 작성하면 사이트가 망가질 수 있습니다. CSP 디렉티브를 이해하고 안전하게 배포하는 방법을 알아보세요.

2026년 4월 14일5분 소요

CSP 생성기: Content Security Policy, 이제 무섭지 않다

처음 CSP를 도입하려 할 때의 기억이 난다. 헤더 하나 추가했더니 사이트의 Google Analytics가 작동을 멈추고, Stripe 결제 창이 안 열리고, 심지어 인라인 스타일로 된 드롭다운 메뉴가 모두 사라졌다. 결국 CSP를 제거하고 "나중에 다시..."라며 미뤘다.

그게 잘못된 접근법이었다. CSP는 강력하지만, 제대로 이해하지 않고 배포하면 당연히 사이트가 망가진다. CSP 생성기로 쉽게 헤더 값을 만들 수 있지만, 각 디렉티브가 무엇을 하는지 이해해야 올바르게 설정할 수 있다.

CSP가 왜 필요한가

XSS(Cross-Site Scripting)는 공격자가 웹페이지에 악성 스크립트를 주입하는 공격이다. 사용자 댓글, 폼 입력값, URL 파라미터 등을 통해 악성 JavaScript가 삽입되면, 그 코드가 피해자의 브라우저에서 실행된다.

결과는 끔찍하다: 세션 쿠키 탈취, 키로깅, 페이지 콘텐츠 변조, 피싱 리다이렉트. 2024년에도 XSS는 OWASP Top 10에 올라있는 여전히 현재 진행형 위협이다.

CSP는 브라우저에게 "이 페이지에서는 다음 출처의 스크립트만 실행해"라고 알려준다. 공격자가 스크립트를 주입해도 허용 목록에 없으면 브라우저가 실행 자체를 거부한다.

브라우저가 CSP를 어떻게 적용하는가

서버가 CSP 헤더를 보내면 브라우저는 모든 리소스 요청을 정책과 대조한다. 허용 목록에 없는 출처의 스크립트, 스타일, 이미지를 로드하려 하면 브라우저가 차단하고 콘솔에 오류를 기록한다.

이게 핵심이다: CSP는 HTTP 헤더로 전달되는 선언적 정책이다. 코드를 수정하는 게 아니라 브라우저의 동작을 제어하는 것이다.

핵심 디렉티브 이해하기

default-src

모든 디렉티브의 기본값 역할을 한다. 특정 디렉티브가 없으면 default-src가 적용된다.

Content-Security-Policy: default-src 'self'

이것만 넣으면 자신의 도메인에서만 모든 리소스를 허용한다. 외부 CDN이나 API가 있다면 다른 디렉티브로 명시해야 한다.

script-src

JavaScript 소스를 제어한다. 가장 중요하면서 가장 까다로운 디렉티브.

script-src 'self' https://www.googletagmanager.com
  • 'self' — 동일 출처
  • 'none' — 아무것도 허용 안 함
  • 'unsafe-inline' — 인라인 스크립트 허용 (XSS 방어 약화)
  • 'unsafe-eval' — eval() 허용
  • 'strict-dynamic' — 신뢰된 스크립트가 동적으로 추가한 스크립트도 허용

style-src

CSS 소스를 제어한다.

style-src 'self' 'unsafe-inline' https://fonts.googleapis.com

style에서 'unsafe-inline'은 script보다 덜 위험하다. CSS 주입으로 할 수 있는 피해가 JavaScript 주입보다 제한적이기 때문이다. 그래도 가능하면 피하는 게 좋다.

img-src

이미지 소스를 제어한다.

img-src 'self' data: https://cdn.example.com *
  • data: — data: URL로 인라인된 이미지 허용
  • blob: — blob URL 허용 (canvas에서 생성된 이미지 등)

connect-src

fetch, XHR, WebSocket, EventSource 연결을 제어한다. API를 호출한다면 반드시 설정해야 한다.

connect-src 'self' https://api.example.com wss://realtime.example.com

frame-src와 frame-ancestors

  • frame-src: 이 페이지에서 iframe으로 삽입할 수 있는 출처
  • frame-ancestors: 이 페이지를 iframe으로 삽입할 수 있는 출처 (클릭재킹 방어)
frame-src https://www.youtube.com https://player.vimeo.com
frame-ancestors 'none'

frame-ancestors 'none'은 X-Frame-Options: DENY와 동일한 효과다.

큰따옴표 vs 작은따옴표 — 왜 헷갈리나

CSP에서 따옴표 규칙:

  • 'self', 'none', 'unsafe-inline', 'unsafe-eval'항상 작은따옴표
  • https://example.com — 따옴표 없음
  • 헤더 전체는 큰따옴표나 따옴표 없이 서버 설정에서 처리
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'";

'self'를 작은따옴표 없이 self로 쓰면, CSP는 이것을 호스트명으로 해석한다. 실수로 self라는 도메인만 허용하게 되는 것이다.

unsafe-inline: 쓰면 안 되는가

'unsafe-inline'을 script-src에 넣으면 CSP의 XSS 방어 효과가 크게 떨어진다. 인라인 스크립트를 모두 허용하기 때문이다.

그런데 현실적으로 피하기 어려운 경우가 있다. 특히:

  • 서드파티 위젯이 <script> 태그로 제공될 때
  • 레거시 코드에 수많은 onclick 핸들러가 있을 때
  • 일부 React/Vue 서버사이드 렌더링 설정

더 안전한 대안은 nonce다:

<script nonce="rAnd0mV4lu3">
  // 이 스크립트는 허용됨
</script>
Content-Security-Policy: script-src 'self' 'nonce-rAnd0mV4lu3'

nonce는 요청마다 새로운 랜덤 값을 생성해야 하므로 서버사이드 렌더링이 필요하다.

Report-Only 모드: 필수 단계

CSP를 처음 도입할 때 가장 중요한 원칙: 절대로 Report-Only 없이 바로 적용하지 마라.

Content-Security-Policy-Report-Only 헤더를 사용하면 정책 위반을 보고하지만 실제로 차단하지 않는다:

add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report";

이렇게 설정하고 사이트를 평소처럼 사용하면 브라우저 콘솔에 차단될 요청들이 표시된다. 이 정보를 토대로 정책을 조정한 후 실제 Content-Security-Policy로 전환한다.

실제 사이트별 CSP 예시

정적 사이트 (CDN 없음)

Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; upgrade-insecure-requests

Google Analytics 포함

Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com; img-src 'self' https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com

Stripe 결제 포함

Content-Security-Policy: default-src 'self'; script-src 'self' https://js.stripe.com; frame-src https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:

Next.js 앱

next.config.js에서 설정:

const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'"
          }
        ]
      }
    ]
  }
}

Next.js는 'unsafe-eval'이 필요한 경우가 있다 (개발 환경의 HMR). 프로덕션에서는 제거하거나 nonce를 쓰자.

자주 하는 실수

1. 서드파티 스크립트를 빠뜨린다

Google Tag Manager, Intercom, HubSpot, Hotjar... 이런 서비스들이 몇 개나 있는지 확인해보면 생각보다 많다. 다 허용 목록에 넣어야 한다.

2. 동적으로 로드되는 스크립트를 잊는다

GTM은 다른 스크립트를 동적으로 로드한다. script-src에 GTM 도메인만 넣으면 GTM이 로드하는 스크립트들이 차단된다. 'strict-dynamic'이나 추가 도메인을 넣어야 한다.

3. CDN 이미지를 빠뜨린다

img-src 'self'만 설정하면 외부 CDN의 이미지가 모두 차단된다. img-src 'self' https://cdn.example.com 처럼 명시해야 한다.

4. 리포트 없이 바로 적용한다

이미 설명했지만 한 번 더: Report-Only를 먼저 써라.

CSP 생성기로 빠르게 시작하기

CSP 생성기에서 보안 수준 프리셋 중 하나를 선택하고, 필요한 디렉티브를 칩 클릭으로 설정하고, 커스텀 도메인을 추가한 후 복사하면 끝이다.

Strict 프리셋은 default-src 'none'에서 시작해서 필요한 것만 추가하는 방식이고, Moderate는 default-src 'self'를 기본으로 주요 설정을 포함한다.

생성된 CSP 값을 Report-Only 헤더로 먼저 적용해서 테스트하고, 문제없으면 실제 헤더로 전환하는 것을 잊지 말자.

결론

CSP는 설정하기 까다롭지만, 웹 보안에서 가장 효과적인 방어 레이어 중 하나다. 한번 제대로 설정하면 XSS 공격에 대한 근본적인 방어막이 생긴다.

핵심만 기억하자:

  1. Report-Only로 먼저 테스트한다
  2. 'self'는 항상 작은따옴표와 함께
  3. 'unsafe-inline'은 script-src에서 가능한 피한다
  4. 서드파티 서비스는 전부 허용 목록에 넣는다

CSP 생성기로 설정을 시작하고, 이 글을 참고해서 각 디렉티브의 의미를 이해하면서 조정하자.

자주 묻는 질문

이 글 공유하기

XLinkedIn

관련 글