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

빌드노트

script-src에 허용한 주소인데 script 태그는 막힌다

script-src에 허용한 주소라도 script-src-elem이 있으면 script 요소는 그 목록을 따른다. 7개 loopback 브라우저 비교로 지시문 우선순위와 default-src fallback을 확인하고, 실제 문서 응답에서 수정할 목록을 찾는 방법을 설명한다.

외부 JS 주소를 script-src에 넣었는데도 <script src="…">가 CSP에 막힌다면, 같은 정책에 script-src-elem이 있는지 확인한다. script-src-elem이 있으면 script 요소는 그 목록으로 판단한다. script-src의 허용 주소가 여기에 자동으로 합쳐지지 않는다. script-src에 주소를 더 넣어도 바뀌지 않는 이유가 여기에 있다.

MDN의 script-src-elem 설명 (새 탭에서 열림)은 이 지시문을 <script> 요소의 출처 제한으로 정의한다. 같은 정책 안에서 확인할 순서는 script-src-elem → script-src → default-src다. 앞의 지시문이 없을 때 다음 지시문을 대신 쓰는 것을 fallback이라고 한다. 앞의 목록에서 거절됐다고 다음 목록의 허용 주소를 찾아가는 방식은 아니다.

차단된 페이지의 응답 헤더부터 확인한다

자기 앱에서는 개발자 도구를 열고 다음 순서로 확인한다.

  1. Console에서 차단된 스크립트 URL과 CSP 오류가 인용하는 지시문을 읽는다.
  2. Network를 연 상태로 페이지를 새로 불러오고, 그 스크립트를 포함하는 HTML 문서의 응답을 선택한다. Response Headers의 Content-Security-Policy가 확인 대상이다. JS 파일 응답의 헤더만 살펴보면 페이지에 적용된 정책을 놓칠 수 있다.
  3. 그 정책에 script-src-elem이 있으면 해당 허용 목록과 실패한 URL을 비교한다. 없으면 script-src, 그것도 없으면 default-src를 확인한다. 수정할 대상은 이 순서로 선택된 지시문이다.

예를 들어 로컬 비교에서는 페이지를 http://127.0.0.1:12026에서 열고, 다른 포트인 http://127.0.0.1:12027에서 probe.js를 제공했다. 다음은 스크립트가 막힌 문서 응답의 CSP다. script-src에 스크립트 서버 주소가 이미 있는지와 script-src-elem의 값을 함께 볼 수 있다.

Content-Security-Policy: script-src http://127.0.0.1:12027; script-src-elem 'self'

이 조건에서 브라우저가 남긴 스크립트 차단 오류는 다음과 같다. 헤더 전체가 아니라 오류 안에 인용된 script-src-elem 'self'가 원인을 좁힌다.

Loading the script 'http://127.0.0.1:12027/probe.js?case=elem_self' violates the following Content Security Policy directive: "script-src-elem 'self'". The action has been blocked.

이 페이지의 'self'는 페이지 자신의 origin을 가리킨다. origin은 스킴·호스트·포트의 조합이므로, 이번처럼 포트가 다른 스크립트 서버는 포함되지 않는다. MDN의 origin 정의 (새 탭에서 열림) script-src가 그 서버를 허용해도 명시된 script-src-elem 'self'에는 들어 있지 않다.

그 스크립트 서버를 허용해야 한다면 기존 script-src-elem 값에 필요한 출처를 넣는다. 다음은 로컬 비교에서 실제로 실행을 허용한 응답이다. script-src는 그대로 두고 script-src-elem만 바꿨다.

Content-Security-Policy: script-src http://127.0.0.1:12027; script-src-elem 'self' http://127.0.0.1:12027

자기 개발 환경에서는 문서 응답을 만드는 서버나 프록시의 기존 지시문을 수정하고, 주소는 허용할 스크립트의 실제 출처로 바꾼다. Network의 Disable cache를 켜고 페이지를 다시 불러온다. 새 문서 응답에 수정값이 실렸는지 확인한 뒤, 같은 스크립트의 실행과 차단 메시지를 다시 본다. 해당 URL의 CSP 오류가 사라졌는데 기능이 동작하지 않으면 Console의 문법 오류나 실행 중 예외를 이어서 확인한다.

같은 파일에 지시문만 바꿔 보니

위 비교는 운영 장애 기록이 아니라 별도로 만든 합성 실험이다. 외부 파일이라는 말은 HTML 밖의 JS 파일을 뜻하며, 인터넷의 스크립트는 사용하지 않았다. loopback 서버 두 개에서만 문서와 JS를 제공했다.

모든 조건에서 같은 JS 바이트를 사용했다. 문서의 marker 문단은 처음에 pending이고, JS가 실행되면 script-ran으로 바뀌며 콘솔에 synthetic-script-ran을 남긴다. 조건마다 새 브라우저 컨텍스트를 만들고, 문서 응답 헤더·DOM·콘솔과 스크립트 서버가 받은 GET 수를 함께 수집했다. URL의 case 쿼리는 수신 기록을 구별하는 표식이며 반환되는 JS 내용은 같다.

다음은 7개 조건의 실제 결과를 묶은 것이다. 아래의 S는 표기를 줄이기 위한 http://127.0.0.1:12027이며, 헤더에 문자 S를 넣은 것은 아니다. 수신 수는 해당 조건의 probe.js GET만 센 값이다.

문서 응답 CSP 값                                            문단         수신
script-src S                                               script-ran   1회
script-src S; script-src-elem 'self'                        pending      0회
script-src S; script-src-elem 'self' S                      script-ran   1회
default-src S                                              script-ran   1회
default-src 'self'                                         pending      0회
default-src S; script-src 'self'                            pending      0회
default-src 'none'; script-src 'none'; script-src-elem S     script-ran   1회

처음 세 조건에서는 script-src의 주소를 유지했다. script-src-elem 'self'를 추가하자 실행이 막혔고, 그 목록에 스크립트 서버를 넣자 다시 실행됐다. 막힌 조건에는 해당 파일의 CSP 오류가 있었고 서버 수신은 0회였다. 허용 조건에는 실행 로그가 남았고 해당 스크립트의 CSP 차단 오류는 없었다.

두 스크립트 지시문을 모두 뺀 네 번째와 다섯 번째 조건은 default-src의 목록에 따라 결과가 갈렸다. 여섯 번째에서는 default-src에 허용 주소가 있어도 script-src 'self'가 선택돼 막혔다. 마지막 조건은 default-src와 script-src가 모두 'none'이어도 명시한 script-src-elem으로 이 외부 script 요소를 허용했다.

따라서 이 비교에서 확인한 것은 같은 정책 안에서 더 구체적인 지시문을 선택한다는 것이다. 세 허용 목록을 합치거나 세 목록을 모두 통과시키는 구조로 읽으면 결과를 설명할 수 없다. CSP Level 3 작업 초안의 fallback 목록과 선택 절차 (새 탭에서 열림)도 이 순서를 정의한다.

오류에 script-src-elem이 보여도 헤더를 다시 읽는다

default-src 'self'만 보낸 조건의 위반 이벤트에서도 effectiveDirective는 script-src-elem이었다. 그 값만 보고 응답에 script-src-elem이 명시됐다고 단정하면 안 된다. 이때 콘솔이 인용한 실제 정책은 default-src 'self'였고, script-src-elem을 명시하지 않아 default-src를 대신 사용했다는 안내가 붙었다. 오류가 가리키는 적용 대상과 응답에 실제 적힌 지시문을 함께 확인해야 한다.

선택된 목록을 수정했는데도 같은 URL이 계속 CSP에 막힌다면 다른 강제 CSP 헤더나 HTML의 CSP meta 정책도 확인한다. 여러 정책은 각각 적용된다. 기존 차단 정책을 둔 채 허용 헤더를 하나 더 추가해도 그 차단을 해제하지 못한다. 이는 CSP Level 3의 다중 정책 설명 (새 탭에서 열림)에 따른 다음 확인 사항이며, 이번 실험은 강제 CSP 헤더 하나만 사용했다.

2026년 10월 22일 직접 실행한 환경은 Windows 11, Python 3.12.10, Playwright 1.59.0, headless Chromium 147.0.7727.15다. 일반 외부 script 요소와 출처 목록의 우선순위만 비교했으며 브라우저와 서버는 실행 뒤 종료했다. 다른 브라우저 엔진, 운영 프록시, 리다이렉트, nonce·hash·strict-dynamic 조합, 모듈의 추가 로딩은 검증하지 않았다. onclick 같은 이벤트 속성은 script-src-attr의 대상이므로 이 결과를 그대로 적용하지 않는다. MDN의 지시문 적용 범위 (새 탭에서 열림)

← 전체 글 보기