이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • SameSite와 CSP는 각각 어떤 공격면을 줄이는가?
  • 보안 헤더를 “있다/없다”가 아니라 응답 계약으로 관리하려면 무엇을 봐야 하는가?
  • CSP를 report-only에서 enforce로 전환할 때 어떤 로그와 테스트가 필요한가?

개요

SameSite는 쿠키 전송 정책이다.

CSP는 Content Security Policy로, 브라우저가 어떤 리소스와 script를 로드하거나 실행할 수 있는지 제한하는 정책이다.

둘은 서로 다른 문제를 다룬다. SameSite는 CSRF 위험을 줄이고, CSP는 XSS 성공 후 실행 가능한 범위를 줄인다.

보안 헤더는 프론트 장식이 아니라 서버 응답 계약이다. API, HTML, admin, static asset 응답별로 필요한 헤더가 다를 수 있다.

공격 시나리오

서비스가 다음 헤더를 반환한다.

Set-Cookie: SESSION=opaque; SameSite=None; Secure
Content-Security-Policy: default-src *; script-src * 'unsafe-inline' 'unsafe-eval'

SameSite가 None이라 cross-site 요청에 session cookie가 전송될 수 있다. CSP는 너무 넓어 XSS가 성공했을 때 외부 script 실행을 거의 제한하지 못한다.

공격자는 CSRF로 이메일 변경을 시도하고, 별도 저장 XSS 지점에서는 외부 script를 로드해 사용자 행동을 훔친다.

취약한 요청/응답 예시

HTTP/1.1 200 OK
Set-Cookie: SESSION=abc; Path=/; SameSite=None
Content-Security-Policy: default-src *; script-src * 'unsafe-inline'
Content-Type: text/html

문제는 헤더가 없다는 것이 아니라, 헤더가 있어도 정책이 너무 넓다는 점이다.

주제별 핵심 판단

의미주의점
Strictcross-site 요청에 거의 전송하지 않는다로그인 callback, 외부 링크 UX가 깨질 수 있다
Laxtop-level safe navigation 등 일부 전송많은 서비스의 기본값으로 적합하지만 완전한 CSRF 방어는 아니다
Nonecross-site 전송 허용Secure가 필요하고 CSRF 위험을 별도로 봐야 한다

인증 session cookie는 Lax 또는 Strict를 우선 검토한다. OAuth callback, 결제 redirect, 임베드 요구사항이 있으면 예외를 문서화한다.

개선된 요청/응답 예시

좋은 CSP는 짧고 명확하게 시작한다.

CSP 적용 기준은 다음처럼 좁게 시작한다.

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

자주 확인할 directive는 다음과 같다.

  • default-src: 기본 리소스 출처
  • script-src: script 로딩과 실행
  • object-src: plugin 계열 차단
  • base-uri: base tag 악용 제한
  • frame-ancestors: clickjacking 방어
  • img-src, connect-src, style-src: 실제 프론트 리소스 요구 반영

unsafe-inline, unsafe-eval, *는 방어 가치를 크게 낮춘다. 필요한 경우 이유와 제거 계획을 남긴다.

방어 설계

응답 유형별 보안 헤더 계약을 둔다.

HTML app:
  Set-Cookie: HttpOnly; Secure; SameSite=Lax
  Content-Security-Policy: default-src 'self'; ...
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
 
admin app:
  SameSite=Strict 우선 검토
  frame-ancestors 'none'
  더 좁은 connect-src
 
JSON API:
  Content-Type: application/json
  X-Content-Type-Options: nosniff
  CORS는 별도 allowlist

CSP는 바로 enforce하기보다 Content-Security-Policy-Report-Only로 violation을 수집하고, 필요한 리소스를 정리한 뒤 차단 정책으로 전환한다.

Spring/HTTP 예시

@Bean
SecurityFilterChain headers(HttpSecurity http) throws Exception {
    return http
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp.policyDirectives(
                "default-src 'self'; " +
                "script-src 'self' 'nonce-{nonce}'; " +
                "object-src 'none'; " +
                "base-uri 'self'; " +
                "frame-ancestors 'none'"))
            .contentTypeOptions(Customizer.withDefaults())
            .referrerPolicy(referrer -> referrer
                .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)))
        .build();
}

실제 nonce 치환은 view rendering 계층과 함께 설계해야 한다. 고정 문자열 nonce는 의미가 없다.

쿠키는 응답에서 직접 확인한다.

Set-Cookie: __Host-SESSION=opaque; Path=/; HttpOnly; Secure; SameSite=Lax

운영 로그와 감사 지점

level=WARN event=csp_violation_reported result=observed document_uri=/dashboard directive=script-src blocked_uri=https://evil.example source_file=/dashboard trace_id=82ac01d1
level=WARN event=security_header_misconfigured result=blocked header=Set-Cookie cookie=SESSION reason=samesite_none_without_secure environment=prod trace_id=82ac01d2

CSP report는 잡음이 많을 수 있다. 확장 프로그램, 오래된 asset, 실제 공격 후보를 분류하는 운영 기준이 필요하다.

실전 판단 기준

  • session cookie에 HttpOnly, Secure, 적절한 SameSite가 있는가?
  • SameSite는 CSRF 위험을 줄이지만 인증/인가 검사를 대신하지 않는다.
  • CSP는 실제 script 로딩 경로와 nonce/hash 운영 방식을 확인해야 효과가 있다.

테스트 포인트

  • SameSite=None 쿠키에는 Secure가 있는가?
  • CSP에 object-src 'none', base-uri 'self', frame-ancestors가 있는가?
  • unsafe-inline, unsafe-eval, *가 있으면 사유와 제거 계획이 있는가?
  • CSP report-only 로그를 보고 enforce 전환 절차를 거쳤는가?
  • HTML, admin, API 응답별 헤더 계약이 테스트되는가?

위험 신호!

  • 보안 헤더 존재 여부만 체크하고 값의 의미를 보지 않는다.
  • 모든 cookie에 일괄 SameSite=None을 준다.
  • CSP가 default-src *unsafe-inline으로 사실상 열려 있다.
  • nonce가 요청마다 바뀌지 않는다.
  • report-only를 영구 운영하고 enforce로 전환하지 않는다.
  • API 응답이 text/html로 나가거나 nosniff가 없다.

확인 질문

확인 질문

  • SameSite와 CSP의 차이는 무엇인가?
    • SameSite는 cookie 전송 정책이고, CSP는 리소스 로딩과 script 실행 정책이다.
  • CSP를 report-only로 먼저 적용하는 이유는 무엇인가?
    • 실제 서비스 리소스 의존성을 확인하고 오탐 차단을 줄인 뒤 enforce로 전환하기 위해서다.
  • 보안 헤더를 응답 계약으로 관리해야 하는 이유는 무엇인가?
    • HTML, admin, API, static asset마다 필요한 정책이 다르고 배포 중 누락되면 브라우저 방어선이 약해지기 때문이다.

참고 문서