본문으로 건너뛰기

33. CSP 우회 노트: nonce, strict-dynamic, 그리고 실수들

CSP는 XSS를 없애는 마법의 필터가 아니라 브라우저가 스크립트의 신뢰 경계를 집행하도록 만드는 선언이다. 따라서 우회 분석의 첫 질문은 “어떤 문자열을 막았나?”가 아니라 “신뢰된 스크립트가 무엇을 로드하고, 데이터가 어떤 싱크에 도달하는가?”여야 한다. 실습은 자기 소유 애플리케이션과 허가된 CTF에서만 한다.

nonce와 strict-dynamic을 정확히 읽기

nonce는 응답마다 새로 생성되는 예측 불가능한 값이어야 하며, 템플릿 캐시가 HTML과 nonce를 함께 재사용하면 목적을 잃는다. nonce가 있다는 사실만으로 DOM 기반 XSS가 사라지는 것도 아니다. 신뢰된 스크립트가 사용자 입력을 innerHTML, eval, 위험한 URL 로더에 전달하면 애플리케이션 코드가 정책의 신뢰 경계를 악용할 수 있다.

'strict-dynamic'은 nonce 또는 해시로 신뢰된 스크립트가 동적으로 삽입한 스크립트를 신뢰하는 동작을 지원한다. 브라우저별 소스 목록 무시 규칙이 달라질 수 있으므로, script-src의 전체 정책과 대상 브라우저를 함께 검증해야 한다. unsafe-inline은 인라인 스크립트를 허용하는 값이고, nonce와 함께 있다고 안전해지는 것이 아니다. 반면 'unsafe-hashes'는 특정 이벤트 핸들러 해시를 허용하는 별도 의미이므로 혼동하지 않는다.

JSONP나 넓은 신뢰 도메인은 정책과 애플리케이션의 결합 지점이다. script-src https://cdn.example가 그 CDN의 모든 경로를 신뢰한다면, 업로드·사용자 콘텐츠·오래된 JSONP 엔드포인트가 공격 표면이 될 수 있다. “CSP에 도메인이 있다”가 아니라 그 도메인에서 실행 가능한 응답을 통제하는 주체가 누구인지 확인한다.

안전한 점검 순서

응답마다 nonce가 바뀌는지, nonce가 HTML 외부에 노출되지 않는지, Content-Security-Policy-Report-Only 보고서가 어떤 위반을 기록하는지 확인한다. 이어서 base-uri, object-src, frame-ancestorsdefault-src의 빈틈을 살핀다. 테스트 문자열은 실제 공격 코드를 넣기보다 무해한 표시와 브라우저 콘솔 관찰로 충분하다. 정책 효과는 헤더 파서와 브라우저에서 모두 확인해야 한다.

방어는 템플릿에서 사용자 입력을 코드로 승격하지 않는 데서 시작한다. nonce는 요청 단위로 생성하고 CSP를 최소 권한으로 줄이며, Trusted Types와 출력 인코딩으로 위험한 DOM 싱크를 별도 통제한다. CSP의 품질은 긴 헤더가 아니라, 허용된 스크립트 목록과 실제 런타임 로딩 그래프가 일치하는지로 평가한다.