Examples
EN

Content Security Policy

[csp] writes a Content-Security-Policy for every page Hwaro builds. It hashes each inline <script> and <style> in the final HTML and lists the hashes in the policy, so the site can drop 'unsafe-inline' and still run its own inline code. A script injected into a page later (an XSS payload, a compromised third-party tag) has no matching hash and is blocked.

Quick Start

[csp]
enabled = true

This writes a _headers file to the output root, with one rule per page:

/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 and Cloudflare Pages read this file. On any other host, use mode = "meta", which puts the policy in each page instead.

Configuration

[csp]
enabled = false                  # default off
mode = "headers"                 # headers | meta
headers_file = "_headers"        # headers mode: path under the output root
report_only = false              # headers mode: Content-Security-Policy-Report-Only

[csp.directives]                 # merged over the defaults below
img-src = "'self' data: https://images.example.com"
Key Default Meaning
enabled false Turn CSP generation on. When it is off the output is unchanged.
mode "headers" headers writes a _headers file; meta injects a <meta> tag into each page.
headers_file "_headers" The headers file, relative to the output directory.
report_only false Send Content-Security-Policy-Report-Only instead, so violations are reported but not blocked. Headers mode only: browsers ignore a report-only policy in <meta>, so report_only with mode = "meta" is a config error.
[csp.directives] see below Directive name = source list, as one string.

Directives

Without [csp.directives] every page gets:

Directive Value
default-src 'self'
script-src 'self' + the page's script hashes
style-src 'self' + the page's style hashes
img-src 'self' data:
font-src 'self'
connect-src 'self'
object-src 'none'
base-uri 'self'
frame-ancestors 'self'

A directive you set replaces the default. For script-src and style-src your value is kept and the page's hashes are added after it. An empty string removes a default directive (frame-ancestors = ""); any other directive with an empty value is written without sources, which is how you add upgrade-insecure-requests = "". Values may not contain ; or ,.

Allowing Inline Code Hwaro Cannot Hash

Browsers ignore 'unsafe-inline' in a directive that also lists a hash. So when your script-src or style-src contains 'unsafe-inline', Hwaro adds no hashes to that directive; with 'unsafe-inline' in style-src it also writes no style-src-attr. This is the opt-out for features that create styles while the page runs, which no hash can cover: Mermaid, MathJax and the gist and tweet shortcodes.

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

Only styles are relaxed here; inline scripts still need their hashes.

What Gets Hashed

Hwaro reads every HTML page it wrote, after all other output processing: --minify, the [privacy] rewrite, AMP links, taxonomy pages, the 404 page and alias redirects. The hash is sha256 over the bytes between the tags, exactly as the browser will hash them. Each page gets only its own hashes.

Content inside HTML comments, <textarea> and <title> is not markup to the browser, so nothing in it is hashed. <noscript> content is hashed: with JavaScript off, its styles apply and the policy still checks them.

Hosts for Hwaro's Features

When a page uses a feature that loads from another host, that host is added to the page's policy. A host is added only when its URL is in the page's final HTML, so a CDN that [privacy] moved to your own site, or a shortcode the page does not use, adds nothing.

Feature Added
[highlight] with 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 shortcode frame-src https://www.youtube.com
vimeo shortcode frame-src https://player.vimeo.com
gist shortcode script-src https://gist.github.com, style-src https://github.githubassets.com, img-src https://gist.github.com https://gist.githubusercontent.com
tweet shortcode script-src, frame-src https://platform.twitter.com
codepen shortcode frame-src https://codepen.io

The gist and tweet rows also match the shortcode's own markup (class="sc-gist", class="twitter-tweet"). [privacy] can localize their scripts, but those scripts still load styles and frames from the original host at run time.

Mermaid, MathJax, gist and tweet also add styles at run time, so they need the style opt-out.

A directive that is not set starts from the value the browser would have fallen back to (frame-src from child-src, then default-src), so adding a frame host keeps 'self'.

Anything else your templates load from another host needs its own entry in [csp.directives].

Headers Mode

mode = "headers" writes one rule per page to headers_file, in the format Netlify and Cloudflare Pages read. Paths are the page URLs and include the base_url subpath: /, /blog/hello/, /404.html. They are encoded exactly like the page's URL in the sitemap, as browsers request them: # becomes %23, ? becomes %3F, and spaces and non-ASCII characters are percent-encoded.

If static/_headers exists, it is not replaced. Hwaro writes your file first, unchanged, and appends its own rules after a # Content-Security-Policy generated by Hwaro ([csp]) comment. When your file already has a block for a page's path that sets Content-Security-Policy (or Content-Security-Policy-Report-Only with report_only), your block wins and Hwaro writes no rule for that path.

Hwaro logs each path where your block replaced its rule.

Hosts combine every rule that matches a request. A /* rule of your own that also sets Content-Security-Policy is therefore sent as a second policy, and the browser enforces both, so do not set one alongside [csp]. Hwaro warns when your file has a CSP block on a path with * or :.

No rule can match some paths. Hosts read :name in a path as a placeholder and * as a wildcard, a control character breaks the file, and a % in a file name does not tell which URL the page has (a%b is published for both /a%b/ and /a%25b/). Such a page gets its policy as a <meta> tag instead, without frame-ancestors, report-uri and sandbox, and Hwaro warns and names it. Rename it to get a rule.

Rules are per page because the format cannot list several paths in one rule. On this documentation site (about 160 pages in two languages) the file is about 50 KB. Cloudflare Pages reads at most 100 rules; Hwaro warns when the file has more. Netlify has no such limit. For larger sites on Cloudflare, use meta mode.

Cloudflare Pages also limits a single header to 2000 characters. Each hash adds about 54, so Hwaro warns about pages whose policy is longer.

A host serves 404.html at the missing URL, where no rule matches, so the 404 page gets no policy in headers mode. Likewise, Cloudflare Pages redirects /page.html to /page, so a rule for a page published as a file (/404.html, or an alias such as aliases = ["/old.html"]) may never match there and the page gets no policy. Use meta mode if those pages matter.

Meta Mode

mode = "meta" injects the policy into each page as the first child of <head>. When a charset declaration (<meta charset> or <meta http-equiv="Content-Type">) comes before any script, style or link, the policy goes right after it, because the declaration must stay within the first 1024 bytes:

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

Browsers ignore frame-ancestors, report-uri and sandbox in a <meta> policy, so Hwaro leaves them out, and warns when you set one of them yourself. Pages without a <head> get no policy, and Hwaro warns about them.

Hwaro treats a <meta http-equiv="Content-Security-Policy"> at that position as its own and replaces it on every build. Do not add a CSP <meta> of your own to templates while [csp] is on.

Builds, Cache and Serve

Limitations