예제
KO

콘텐츠 보안 정책(CSP)

[csp]는 Hwaro가 빌드하는 모든 페이지에 Content-Security-Policy를 만들어 줍니다. 최종 HTML에 있는 인라인 <script>와 <style>을 하나씩 해시해서 정책에 넣기 때문에, 'unsafe-inline' 없이도 사이트의 인라인 코드는 그대로 동작합니다. 나중에 페이지에 끼어든 스크립트(XSS 페이로드, 침해된 서드파티 태그 등)는 맞는 해시가 없으므로 차단됩니다.

빠른 시작

[csp]
enabled = true

이렇게 설정하면 출력 루트에 페이지마다 규칙 하나씩을 담은 _headers 파일이 생깁니다.

/blog/hello/
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-hI0C…='; style-src 'self' 'sha256-G7+h…='; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

Netlify와 Cloudflare Pages가 이 파일을 읽습니다. 다른 호스트에서는 정책을 각 페이지 안에 넣는 mode = "meta"를 쓰세요.

설정

[csp]
enabled = false                  # 기본값은 꺼짐
mode = "headers"                 # headers | meta
headers_file = "_headers"        # headers 모드: 출력 루트 기준 경로
report_only = false              # headers 모드: Content-Security-Policy-Report-Only

[csp.directives]                 # 아래 기본값 위에 덮어씀
img-src = "'self' data: https://images.example.com"
키 기본값 의미
enabled false CSP 생성을 켭니다. 꺼져 있으면 출력이 바뀌지 않습니다.
mode "headers" headers는 _headers 파일을 쓰고, meta는 각 페이지에 <meta> 태그를 넣습니다.
headers_file "_headers" 헤더 파일 경로. 출력 디렉터리 기준입니다.
report_only false 대신 Content-Security-Policy-Report-Only를 보내서 위반을 막지 않고 보고만 합니다. headers 모드 전용입니다. 브라우저는 <meta>의 report-only 정책을 무시하므로 mode = "meta"와 함께 쓰면 설정 오류입니다.
[csp.directives] 아래 참고 지시어 이름 = 소스 목록(문자열 하나).

지시어

[csp.directives]가 없으면 모든 페이지에 다음 정책이 붙습니다.

지시어 값
default-src 'self'
script-src 'self' + 페이지의 스크립트 해시
style-src 'self' + 페이지의 스타일 해시
img-src 'self' data:
font-src 'self'
connect-src 'self'
object-src 'none'
base-uri 'self'
frame-ancestors 'self'

직접 지정한 지시어는 기본값을 대체합니다. script-src와 style-src는 지정한 값을 유지한 채 그 뒤에 페이지의 해시를 덧붙입니다. 빈 문자열은 기본 지시어를 없애고(frame-ancestors = ""), 그 밖의 지시어에 빈 값을 주면 소스 없이 지시어만 씁니다. upgrade-insecure-requests = ""는 이렇게 추가합니다. 값에는 ;나 ,를 넣을 수 없습니다.

Hwaro가 해시할 수 없는 인라인 코드 허용하기

해시가 하나라도 있는 지시어에서는 브라우저가 'unsafe-inline'을 무시합니다. 그래서 script-src나 style-src에 'unsafe-inline'이 있으면 Hwaro는 그 지시어에 해시를 넣지 않고, style-src에 있으면 style-src-attr도 쓰지 않습니다. 페이지가 실행되는 동안 스타일을 만들어서 어떤 해시로도 허용할 수 없는 기능, 즉 Mermaid, MathJax, gist와 tweet 숏코드를 위한 예외 설정입니다.

[csp.directives]
style-src = "'self' 'unsafe-inline'"

이렇게 하면 스타일만 느슨해지며, 인라인 스크립트에는 여전히 해시가 필요합니다.

해시 대상

Hwaro는 다른 출력 처리가 모두 끝난 뒤 자신이 쓴 HTML 페이지를 전부 읽습니다. --minify, [privacy] 재작성, AMP 링크, 택소노미 페이지, 404 페이지, 별칭 리다이렉트까지 반영된 상태입니다. 해시는 태그 사이 바이트의 sha256으로, 브라우저가 계산하는 값과 정확히 같습니다. 페이지마다 자기 해시만 받습니다.

HTML 주석, <textarea>, <title> 안의 내용은 브라우저에게 마크업이 아니므로 해시하지 않습니다. <noscript> 안의 내용은 해시합니다. JavaScript가 꺼져 있으면 그 스타일이 적용되고, 정책도 여전히 검사하기 때문입니다.

Hwaro 기능의 호스트

페이지가 다른 호스트에서 불러오는 기능을 쓰면 그 호스트가 페이지 정책에 추가됩니다. 해당 URL이 페이지의 최종 HTML에 있을 때만 추가하므로, [privacy]가 사이트로 옮긴 CDN이나 페이지가 쓰지 않는 숏코드는 아무것도 추가하지 않습니다.

기능 추가되는 값
[highlight]의 use_cdn = true(cdnjs) script-src, style-src https://cdnjs.cloudflare.com
[markdown] math, KaTeX script-src, style-src, font-src https://cdn.jsdelivr.net
[markdown] math, MathJax script-src, font-src https://cdn.jsdelivr.net
[markdown] mermaid script-src https://cdn.jsdelivr.net
youtube 숏코드 frame-src https://www.youtube.com
vimeo 숏코드 frame-src https://player.vimeo.com
gist 숏코드 script-src https://gist.github.com, style-src https://github.githubassets.com, img-src https://gist.github.com https://gist.githubusercontent.com
tweet 숏코드 script-src, frame-src https://platform.twitter.com
codepen 숏코드 frame-src https://codepen.io

gist와 tweet 행은 숏코드 자체의 마크업(class="sc-gist", class="twitter-tweet")도 찾습니다. [privacy]가 이들의 스크립트를 로컬로 옮겨도, 그 스크립트는 실행 중에 원래 호스트에서 스타일과 프레임을 불러오기 때문입니다.

Mermaid, MathJax, gist, tweet은 실행 중에 스타일도 추가하므로 스타일 예외 설정이 필요합니다.

설정되지 않은 지시어는 브라우저가 대신 썼을 값(frame-src라면 child-src, 그다음 default-src)에서 시작하므로, 프레임 호스트를 추가해도 'self'는 남습니다.

템플릿이 다른 호스트에서 불러오는 그 밖의 것은 [csp.directives]에 직접 넣어야 합니다.

headers 모드

mode = "headers"는 Netlify와 Cloudflare Pages가 읽는 형식으로 headers_file에 페이지마다 규칙을 하나씩 씁니다. 경로는 페이지 URL이며 base_url의 하위 경로를 포함합니다: /, /blog/hello/, /404.html. 경로는 사이트맵에 실리는 페이지 URL과 똑같이, 브라우저가 요청하는 대로 인코딩합니다. #은 %23, ?는 %3F가 되고, 공백과 ASCII가 아닌 문자는 퍼센트 인코딩됩니다.

static/_headers가 있으면 덮어쓰지 않습니다. Hwaro는 그 파일을 먼저 그대로 쓰고, # Content-Security-Policy generated by Hwaro ([csp]) 주석 뒤에 자신의 규칙을 덧붙입니다. 사용자 파일에 어떤 페이지 경로의 블록이 이미 있고 그 블록이 Content-Security-Policy(report_only라면 Content-Security-Policy-Report-Only)를 설정하면 사용자 블록이 우선하며, Hwaro는 그 경로의 규칙을 쓰지 않습니다.

사용자 블록이 Hwaro의 규칙을 대신한 경로는 로그에 남깁니다.

호스트는 요청에 맞는 규칙을 모두 합칩니다. 직접 만든 /* 규칙이 Content-Security-Policy를 설정하면 두 번째 정책으로 함께 전송되고 브라우저는 둘 다 적용하므로, [csp]와 함께 쓰지 마세요. 사용자 파일에서 *나 :가 들어간 경로의 블록이 CSP를 설정하면 Hwaro가 경고합니다.

어떤 규칙으로도 맞출 수 없는 경로가 있습니다. 호스트는 경로의 :name을 자리표시자로, *를 와일드카드로 읽고, 제어 문자는 파일을 깨뜨리며, 파일 이름의 %로는 페이지 URL을 하나로 정할 수 없습니다(a%b는 /a%b/와 /a%25b/ 모두에서 게시됨). 이런 페이지는 정책을 대신 <meta> 태그로 받으며(frame-ancestors, report-uri, sandbox 제외), Hwaro가 그 페이지를 밝혀 경고합니다. 규칙을 받으려면 이름을 바꾸세요.

이 형식은 규칙 하나에 여러 경로를 담을 수 없어서 규칙이 페이지마다 하나씩 생깁니다. 이 문서 사이트(두 언어, 약 160페이지)에서는 파일이 약 50 KB입니다. Cloudflare Pages는 규칙을 최대 100개까지만 읽으며, 이를 넘으면 Hwaro가 경고합니다. Netlify에는 이런 제한이 없습니다. Cloudflare에서 큰 사이트라면 meta 모드를 쓰세요.

Cloudflare Pages는 헤더 하나의 길이도 2000자로 제한합니다. 해시 하나가 약 54자를 더하므로, 정책이 이보다 긴 페이지는 Hwaro가 경고합니다.

호스트는 없는 URL에서 404.html을 내보내는데, 그 URL에 맞는 규칙이 없으므로 headers 모드에서 404 페이지에는 정책이 붙지 않습니다. 마찬가지로 Cloudflare Pages는 /page.html을 /page로 리다이렉트하므로, 파일로 게시되는 페이지 (/404.html, 또는 aliases = ["/old.html"] 같은 별칭)의 규칙은 그곳에서 맞지 않을 수 있고 그 페이지에는 정책이 붙지 않습니다. 이런 페이지가 중요하다면 meta 모드를 쓰세요.

meta 모드

mode = "meta"는 정책을 각 페이지의 <head> 첫 자식으로 넣습니다. 스크립트, 스타일, 링크보다 앞에 charset 선언(<meta charset> 또는 <meta http-equiv="Content-Type">)이 있으면 그 바로 뒤에 넣는데, charset 선언은 처음 1024바이트 안에 있어야 하기 때문입니다.

<head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src &apos;self&apos;; …">

브라우저는 <meta> 정책의 frame-ancestors, report-uri, sandbox를 무시하므로 Hwaro는 이를 빼고, 직접 설정했다면 경고합니다. <head>가 없는 페이지에는 정책이 붙지 않으며, Hwaro가 이를 경고합니다.

Hwaro는 그 위치에 있는 <meta http-equiv="Content-Security-Policy">를 자신이 넣은 것으로 보고 빌드할 때마다 바꿉니다. [csp]를 켠 동안에는 템플릿에 CSP <meta>를 직접 넣지 마세요.

빌드, 캐시, serve

제한 사항