CSP Nonce vs Hash vs unsafe-inline — When to Use Each
Inline scripts are blocked by a strict CSP. You have three options to allow them — but only two actually keep you safe.
| Approach | Safe? | Best for | Works for dynamic content? |
|---|---|---|---|
| unsafe-inline | ❌ No | Development only | Yes — but allows all inline scripts |
| Nonce | ✅ Yes | Server-rendered pages | Yes — new nonce per request |
| Hash | ✅ Yes | Static inline scripts | No — content must match exactly |
unsafe-inline — why it defeats CSP
Content-Security-Policy: script-src 'self' 'unsafe-inline'
This allows every inline script to run — including any scripts an attacker manages to inject via XSS. It eliminates the "inline script blocked" console error, but it also eliminates all XSS protection. Never use in production.
Nonces — the right approach for dynamic pages
# Server generates a random nonce each request
nonce = base64.b64encode(os.urandom(16)).decode()
# Header includes nonce
Content-Security-Policy: script-src 'self' 'nonce-{nonce}'
# HTML includes matching nonce attribute
<script nonce="{nonce}">console.log('allowed');</script>
<script>console.log('blocked — no nonce');</script>
An injected script cannot know the nonce (it is random per request), so it is blocked. Your trusted scripts with the attribute are allowed.
Hashes — for static inline scripts
# Compute SHA-256 hash of the exact script content
import hashlib, base64
content = "console.log('hello');"
hash_val = base64.b64encode(hashlib.sha256(content.encode()).digest()).decode()
# Header
Content-Security-Policy: script-src 'self' 'sha256-{hash_val}'
# HTML — no attribute needed, content must match exactly
<script>console.log('hello');</script>
Change one character in the script and the hash no longer matches. Only use for scripts that never change.
Generate your CSP → CSPFixer