본문 바로가기
빌드노트 검색

빌드노트

CSP 위반 메시지가 있는데 스크립트는 실행된다

CSP 위반 메시지와 실제 차단은 다를 수 있다. 같은 script-src 정책을 보고전용과 강제 헤더로 보내 DOM 숫자·콘솔을 비교하고, HTML 응답에서 두 헤더와 실제 실행 흔적을 확인하는 방법을 설명한다.

브라우저 콘솔에 CSP 위반 메시지가 있는데도 스크립트가 화면을 바꾼다면, 먼저 그 HTML 응답의 헤더 이름이 Content-Security-Policy-Report-Only인지 확인한다. Report-Only는 정책을 적용했을 때 발생할 위반을 관찰하는 모드다. 위반을 발견해도 그 정책 때문에 실행을 막지는 않는다.

MDN의 Report-Only 설명 (새 탭에서 열림)은 관찰과 강제 적용을 구분한다. 따라서 콘솔에 위반이 보인다는 사실만으로 스크립트가 차단됐다고 결론 내릴 수 없다. 응답이 전달한 정책과 실제 실행 결과를 함께 봐야 한다.

같은 스크립트에서 헤더 이름만 바꿨다

이 차이를 확인하기 위해 새 브라우저와 loopback 서버에서 숫자 하나만 바꾸는 페이지를 열었다. 실제 서비스 장애 기록이 아니라 별도 비교 실험이다. HTML의 핵심 입력은 다음과 같다.

<p id="count">0</p>
<script>
document.querySelector('#count').textContent = '1';
</script>

세 문서 응답의 HTML 바이트는 같았다. 첫 응답에는 CSP가 없었고, 둘째에는 아래 Report-Only 헤더만 넣었다.

Content-Security-Policy-Report-Only: script-src 'none'

셋째 응답에서는 헤더 이름만 바꿨다. 두 헤더의 정책 값은 똑같이 script-src 'none'이다. 이 값은 스크립트를 허용하지 않아 차이를 확실히 드러내는 실험용 조건이다. 서비스에 필요한 허용 목록을 대신할 설정은 아니다.

Content-Security-Policy: script-src 'none'

브라우저가 받은 응답 헤더와 DOM을 확인한 결과는 다음과 같다. 모든 HTML 응답의 상태 코드는 200이었다.

  • 정책 없음: 숫자가 1로 바뀌었고 CSP 위반 메시지는 없었다.
  • Report-Only: 숫자가 1로 바뀌었고 위반 메시지가 남았다.
  • 강제 CSP: 숫자는 0으로 남았고 실행 차단 메시지가 남았다.

Report-Only의 콘솔 메시지 끝에는 The policy is report-only라는 설명이 있었다. 강제 CSP 메시지는 The action has been blocked.로 끝났다. 두 메시지 모두 앞부분은 inline script가 script-src 'none'을 위반했다는 내용이었다. 위반 여부는 같았지만, 그 위반에 대해 실행을 차단하는지는 달랐다.

관찰용 위반 이벤트의 disposition도 Report-Only에서는 report, 강제 CSP에서는 enforce였다. 페이지 숫자를 바꾸는 코드는 그대로 두고, 이벤트 수집만 브라우저 자동화의 별도 관찰 코드에서 수행했다. CSP Level 3의 inline 검사 절차 (새 탭에서 열림) 역시 위반을 기록하는 단계와 강제 정책일 때 차단 결과를 내는 단계를 구분한다. 이 문서는 확인 당시 2026년 9월 16일자 편집 초안이다.

내 페이지에서는 HTML 응답과 실행 흔적을 맞춘다

개발자 도구를 연 상태로 문제 페이지를 새로 불러온다. Network에서 현재 페이지의 HTML 문서 요청을 고르고 Headers의 Response Headers를 읽는다. JS 파일 응답에 붙은 헤더와 혼동하지 않는다. Chrome DevTools의 헤더 확인 방법 (새 탭에서 열림)

  1. Content-Security-Policy-Report-Only와 Content-Security-Policy를 각각 찾는다. 설정 파일에 적힌 값보다 이번 문서 응답에 실제 도착한 값을 기준으로 판단한다.
  2. Console에서 해당 스크립트 위치의 메시지를 읽는다. 보고전용이라는 설명이 있는지, 실제 차단됐다는 설명이 있는지 확인한다.
  3. 그 스크립트가 바꾸는 DOM이나 남기는 실행 흔적을 확인한다. 메시지를 읽은 문서와 같은 페이지·프레임을 대상으로 본다.

위 실험 페이지에서는 로드 뒤 Console에 아래 식을 입력해 숫자만 읽었다. 스크립트를 다시 실행하거나 DOM 값을 수정하는 입력은 아니다.

document.querySelector('#count').textContent

정책 없음과 Report-Only에서는 문자열 "1", 강제 CSP에서는 "0"이 나왔다. 자기 페이지에서는 #count를 확인하려는 요소의 선택자로 바꾼다. 요소가 없어 오류가 나면 먼저 대상 문서·프레임과 선택자를 확인한다. 이 숫자가 실행 여부의 근거가 되는 이유는 실험에서 값을 바꾸는 코드가 한 곳뿐이기 때문이다. 여러 코드가 같은 영역을 바꾸는 앱에서는 해당 초기화 코드의 중단점이나 고유한 실행 로그로 확인 대상을 좁힌다.

Report-Only 메시지가 있는데 초기화가 멈췄다면, 그 메시지만으로 원인을 확정하지 않는다. 다른 강제 CSP의 차단 메시지와 자바스크립트 예외를 확인한다. 반대로 실행이 계속된다는 이유로 Report-Only 정책이 전달되지 않았다고 판단하지도 않는다. 이번 비교에서는 헤더와 위반 기록이 모두 있었고 코드도 실행됐다.

관찰을 마친 정책과 차단할 정책을 구분한다

새 정책이 필요한 코드를 얼마나 위반하는지 알아보려면 Report-Only가 맞다. 실제 실행을 제한하려면 검토한 정책을 Content-Security-Policy로 전달해야 한다. 개발 환경에서 같은 기능을 다시 실행해, 새 응답의 강제 헤더와 의도한 차단 또는 허용을 확인한다. 콘솔 경고의 존재만으로 전환이 끝났다고 판단하지 않는다.

두 헤더는 함께 올 수도 있다. 기존 강제 정책을 유지하면서 다음 정책을 보고전용으로 관찰할 수 있다. 추가 실험에서 두 헤더 모두에 같은 script-src 'none'을 넣자 숫자는 0이었고, enforce와 report 위반이 각각 남았다. Report-Only 헤더를 추가해도 함께 전달된 강제 정책이 해제되지는 않았다. CSP의 다중 정책 설명 (새 탭에서 열림)은 각 정책이 자기 유형에 따라 적용된다고 설명한다.

또한 콘솔에서 위반을 본 것과 보고서가 서버에 도착한 것은 별개다. 이번 실험에는 report-to, report-uri, 외부 보고 endpoint를 설정하지 않았다. Report-Only에서도 로컬 위반 메시지는 관찰했지만 보고서 전송·수신은 검증하지 않았다. 서버에서 보고서를 모으려면 보고 대상과 수집 설정이 필요하며, 실제 수신을 따로 확인해야 한다. 이 조건은 MDN의 보고 설정 설명 (새 탭에서 열림)에 나와 있다.

직접 비교한 환경은 Windows 11, Python 3.12.10, Playwright 1.59.0, headless Chromium 147.0.7727.15다. 새 브라우저의 loopback HTTP 문서에서 일반 inline script만 확인했다. 다른 브라우저 엔진, 운영 서버·프록시의 헤더 전달, 보고서 수집은 확인하지 않았다. 이 비교가 확인한 것은 같은 정책 값이라도 보고전용과 강제 적용에서 실제 실행 결과가 달라진다는 점이다.

← 전체 글 보기