Why CSP Matters for Static Sites
Static sites built with Hugo are inherently more secure than dynamic applications, but they still execute third-party scripts for analytics, ads, and embeds. A Content Security Policy (CSP) protects your visitors from XSS, data injection, and malicious script execution by controlling which resources can load.
Setting CSP via Cloudflare Pages
Cloudflare Pages supports custom _headers files for controlling HTTP response headers:
# static/_headers
/*
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'
For Hugo, place the _headers file in your static/ directory so it gets copied to the output:
static/
├── _headers
├── css/
└── js/
Building a Realistic CSP for Hugo
A blog that uses Google AdSense, Google Analytics, and embedded YouTube videos needs a more permissive policy. Here’s a production-ready CSP:
# static/_headers
/*
Content-Security-Policy:
default-src 'self';
script-src 'self'
'strict-dynamic'
'sha256-...' (inline script hash)
https://www.googletagmanager.com
https://pagead2.googlesyndication.com
https://www.youtube.com;
style-src 'self'
'sha256-...' (inline style hash)
https://fonts.googleapis.com;
img-src 'self'
https://*.googleapis.com
https://*.googlesyndication.com
https://www.google.com
https://*.doubleclick.net
https://*.youtube.com
data:;
font-src 'self'
https://fonts.gstatic.com;
frame-src 'self'
https://www.youtube.com
https://www.google.com;
connect-src 'self'
https://*.google-analytics.com;
report-uri /csp-report;
Using Nonces vs Hashes
For inline scripts (common in Hugo themes), you have two options:
Nonces (unique per-request):
<script nonce="ABC123">console.log("safe")</script>
CSP: script-src 'nonce-ABC123'
Nonces work best for dynamic sites but require server-side generation, which is difficult for fully static Hugo sites.
Hashes (content-based):
<script>console.log("safe")</script>
CSP: script-src 'sha256-...'
Hashes are ideal for static sites because they remain constant across deploys. Calculate the hash of your inline scripts and add them to the policy:
echo -n 'console.log("safe")' | openssl dgst -sha256 -binary | base64
Understanding strict-dynamic
The 'strict-dynamic' directive is a powerful CSP feature. It tells the browser to trust scripts loaded by an already-trusted script, reducing the need to enumerate all CDN URLs:
script-src 'self' 'strict-dynamic' 'sha256-ABC...';
With strict-dynamic, if your main bundle (trusted via hash) loads a CDN script, that CDN script is automatically trusted. This significantly simplifies CSP management for sites with many third-party scripts.
CSP for Google AdSense
AdSense requires specific CSP allowances. Key directives:
| Directive | Required Sources |
|---|---|
| script-src | https://pagead2.googlesyndication.com, 'unsafe-inline' (or hash) |
| img-src | https://*.googlesyndication.com, https://*.doubleclick.net |
| frame-src | https://googleads.g.doubleclick.net |
Consider using 'unsafe-inline' only as a last resort — hashes are preferable.
CSP Reporting
Configure a report-uri or report-to directive to receive violation reports:
Content-Security-Policy: ...; report-uri /csp-report;
You can use a service like report-uri.com or Cloudflare CSP Analytics to collect and visualize reports. This helps identify blocked resources without breaking functionality.
Testing Your CSP
Before deploying, test with a restrictive policy using Content-Security-Policy-Report-Only:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;
This header enforces nothing but sends violation reports, letting you identify issues safely.
Conclusion
A well-configured CSP protects Hugo blog visitors from XSS and script injection while allowing legitimate third-party services to function. By combining strict-dynamic, script hashes, and incremental deployment via report-only mode, you can build a robust security posture that balances protection with functionality.
