정적 호스팅에서 nonce 없이 CSP 세우기
CSP 를 제대로 세우려면 인라인 스크립트마다 요청별 nonce 를 붙여야 합니다. 그런데 정적 사이트에는 요청을 받는 서버가 없습니다. 그래서 대부분 'unsafe-inline' 을 넣고 끝내는데, 그러면 CSP 가 사실상 없는 것과 같습니다.
정적 호스팅에서 nonce 에 해당하는 것은 해시입니다. 빌드 결과의 인라인 스크립트를 전부 찾아 sha256 을 계산해 정책에 넣으면 됩니다.
어떻게 하나
섹션 제목: “어떻게 하나”빌드가 끝난 뒤 dist/ 를 훑습니다.
const INLINE_SCRIPT = /<script(?![^>]*\ssrc=)[^>]*>([\s\S]*?)<\/script>/gconst INLINE_STYLE = /<style[^>]*>([\s\S]*?)<\/style>/g
const sha256 = (src) => `'sha256-${createHash('sha256').update(src, 'utf8').digest('base64')}'`
for (const file of htmlFiles) { const html = await readFile(file, 'utf8') for (const [, body] of html.matchAll(INLINE_SCRIPT)) scripts.add(sha256(body)) for (const [, body] of html.matchAll(INLINE_STYLE)) styles.add(sha256(body))}해시는 태그 사이의 내용 그대로를 대상으로 합니다. 앞뒤 공백과 줄바꿈까지 포함해서요. 한 글자만 달라도 정책이 그 스크립트를 막습니다.
그리고 _headers 를 만듭니다.
/* Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-...' 'sha256-...'; ...이 파일은 빌드마다 다시 생성돼야 합니다. 손으로 고치면 다음 빌드에 지워지고, 더 나쁘게는 낡은 해시가 남아 스크립트가 막힙니다.
검색과 WebAssembly
섹션 제목: “검색과 WebAssembly”문서 사이트에 검색을 붙이면(Pagefind 같은 것) 정책을 조금 열어야 합니다.
script-src 'self' 'wasm-unsafe-eval' 'sha256-...';worker-src 'self' blob:;색인을 WebAssembly 로 돌리고 워커를 띄우기 때문입니다. 'wasm-unsafe-eval' 은 'unsafe-eval' 과 다릅니다 — WebAssembly 컴파일만 허용하고 문자열을 코드로 실행하는 것은 여전히 막습니다.
인라인이 하나도 없으면
섹션 제목: “인라인이 하나도 없으면”이게 가장 좋은 상태입니다. 이 위키의 자매 사이트인 작업일지는 인라인 스크립트가 하나도 없어서 script-src 가 'self' 뿐입니다. 해시조차 필요 없습니다.
무엇이 깨지는지
섹션 제목: “무엇이 깨지는지”CSP 가 잘못되면 화면은 멀쩡하게 그려지고 기능만 조용히 죽습니다. 문서가 안 보이는 게 아니라 검색 상자에 입력해도 결과가 안 나오는 식입니다. 눈으로는 알아채기 어렵습니다.
그래서 실제 브라우저로 확인해야 합니다.
// 헤더를 실제로 붙인 채 띄우고page.on('console', (m) => m.type() === 'error' && problems.push(m.text()))await page.addInitScript(() => { window.__csp = [] document.addEventListener('securitypolicyviolation', (e) => window.__csp.push(`${e.violatedDirective} blocked ${e.blockedURI}`), )})// 테마 토글을 눌러 보고, 검색창에 입력해 결과가 나오는지 본다securitypolicyviolation 이벤트를 듣는 것이 핵심입니다. 콘솔 오류만 보면 놓치는 것이 있습니다.
배포 검증에도 넣어 두십시오. 라이브 응답의 script-src 에 'unsafe-inline' 이 들어갔는지만 봐도 대부분의 사고를 잡습니다.
나머지 지시어
섹션 제목: “나머지 지시어”외부에서 받아오는 것이 없다면 전부 'self' 로 닫을 수 있습니다.
default-src 'self';img-src 'self' data:;font-src 'self';connect-src 'self';frame-ancestors 'none';base-uri 'self';form-action 'none';object-src 'none';서체를 자체 호스팅하면 font-src 에 외부 출처를 열 필요가 없습니다. 구글 폰트를 쓰면 fonts.googleapis.com 과 fonts.gstatic.com 을 열어야 하고, 그만큼 정책이 넓어집니다.
style-src 는 조금 다릅니다. 요소에 직접 붙는 style 속성은 해시로 잡을 수 없어서 style-src-attr 'unsafe-inline' 을 따로 두게 됩니다. 스크립트 실행과는 무관하므로 위험도가 다릅니다.
