Nonces, hashes and report-only
Why it matters Almost every real site has a few inline scripts of its own. If the only way to keep them working is 'unsafe-inline', CSP stops protecting against injection at all. Nonces and hashes let your scripts in and keep injected ones out. Report-only mode lets you switch a policy on without breaking anything for visitors on day one.
Picture a concert where each night's wristband has a different colour, announced only to staff an hour before doors open. A forged wristband from last week is useless. A nonce is tonight's wristband for your inline scripts.
Nonces
Nonce: short for "number used once". A fresh random value the server creates for every response, puts in the header as 'nonce-VALUE', and also writes into each of its own inline script tags as nonce="VALUE".
Content-Security-Policy: script-src 'self' 'nonce-4fT9qLx2Zb8wRk1e'
<script nonce="4fT9qLx2Zb8wRk1e">startShop();</script> runs
<script>sendCookies();</script> blocked: no nonceAn attacker who injects a script tag does not know this response's value, so their script is refused. Three rules make that true:
- Unpredictable: at least 128 bits from a secure random source, such as Python's
secrets.token_urlsafe(16). - Fresh every response: never a fixed value in a config file, and be careful with page caches that would serve the same nonce to everyone.
- Only on your own tags: a template that adds the nonce to any script tag, including ones built from user input, hands the wristband to the attacker.
This site does it with django-csp: every inline script in the templates is written as <script nonce="{{ request.csp_nonce }}">, and the package puts the matching value in the header.
Hashes
Hash source: 'sha256-BASE64', the SHA-256 fingerprint of the exact text inside a script tag. If the text matches, it runs. Useful for static pages where nothing can add a nonce per response.
import base64, hashlib
code = "startShop();" # exactly what is between the tags
print("'sha256-" + base64.b64encode(hashlib.sha256(code.encode()).digest()).decode() + "'")Every byte counts: an extra space or newline gives a different hash. Hashes and nonces cover inline <script> blocks only, not onclick= attributes (a rarely used keyword, 'unsafe-hashes', can allow one hashed handler, but moving it is cleaner). Move handlers into your script with addEventListener.
Why 'unsafe-inline' is sometimes left in
When a nonce or hash is listed, browsers that understand them ignore 'unsafe-inline'. Very old browsers do not understand nonces, so they fall back to 'unsafe-inline' and the site still works there. That is why floati.life's policy lists both. Without a nonce or hash, though, 'unsafe-inline' lets every injected script run.
One step further is 'strict-dynamic': a script you trusted with a nonce may load further scripts, and browsers that support it then ignore host lists. It suits big apps that load code on the fly. (The practice checker does not model it.)
Rolling out safely: report-only
Content-Security-Policy-Report-Only: a second header with the same syntax. The browser blocks nothing, but reports what it would have blocked, in the console and, if you ask, to an address on your server.
Reporting-Endpoints: csp="https://shopd.example/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-...'; report-to cspreport-to names an endpoint from the Reporting-Endpoints header. Older guides use report-uri https://...; many sites send both, and browsers that understand report-to ignore report-uri. A sensible rollout: send the new policy as report-only for a week or two, fix what it reports, then switch it to the enforcing header. You can send an enforcing policy and a stricter report-only one side by side.
Privacy note: a report includes the page address and the blocked address. Page addresses can carry personal data in their query strings, so send reports to your own server, keep them briefly, and never to an outside analytics service.
A common misconception: "report-only protects visitors while I test". It protects no one. It is a measuring tool. Do not leave a policy in report-only for months.
In other languages
// JavaScript (Node)
const nonce = require("crypto").randomBytes(16).toString("base64");// Go
b := make([]byte, 16); rand.Read(b) // crypto/rand
nonce := base64.StdEncoding.EncodeToString(b)// PHP
$nonce = base64_encode(random_bytes(16));Recap
- A nonce is a fresh, unguessable value per response that marks your own inline scripts.
- A hash allows one exact inline script; any change to its text needs a new hash.
'unsafe-inline'is ignored when a nonce or hash is listed, and dangerous when one is not.- Roll out with
Content-Security-Policy-Report-Only, then enforce; keep reports on your own server.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.