Understanding Content Security Policy
Content Security Policy (CSP) is the most powerful security header available to web developers, capable of preventing cross-site scripting (XSS) attacks—one of the most common and dangerous web vulnerabilities. By controlling which resources browsers can load and execute, CSP creates a whitelist-based security model that blocks malicious code even when attackers find injection vulnerabilities.
In 2025, implementing CSP is no longer optional for security-conscious organizations. Modern best practices emphasize "Strict CSP" using nonces or hashes rather than legacy approaches that rely on domain whitelisting. Understanding these implementation strategies is essential for effective XSS prevention.
How CSP Prevents XSS Attacks
XSS attacks work by injecting malicious JavaScript into websites:
Stored XSS: Attacker saves malicious script to database (via comment form, profile field, etc.) Reflected XSS: Attacker includes script in URL parameters that get echoed on page DOM-based XSS: Client-side JavaScript writes untrusted data to dangerous sinks
Without CSP, browsers execute these injected scripts, allowing attackers to:
- Steal session cookies and credentials
- Perform actions as the victim user
- Deface websites
- Redirect to phishing sites
- Install keyloggers
CSP prevents execution by establishing which scripts are legitimate and blocking everything else.
CSP Directive Syntax
CSP uses directives to control different resource types:
Content-Security-Policy: directive-name source-expression; another-directive source-expression
Key Directives
default-src: Fallback for other fetch directives
script-src: Controls JavaScript sources
style-src: Controls CSS sources
img-src: Controls image sources
font-src: Controls font sources
connect-src: Controls XMLHttpRequest, WebSocket, EventSource
frame-src: Controls iframe sources
frame-ancestors: Controls where the page can be embedded (replaces X-Frame-Options)
base-uri: Restricts URLs in <base> element
form-action: Controls form submission targets
Source Expressions
'none': Block all sources 'self': Allow same origin only https://example.com: Allow specific domain 'unsafe-inline': Allow inline scripts/styles (dangerous, avoid) 'unsafe-eval': Allow eval() and similar (dangerous, avoid) 'nonce-random123': Allow resources with matching nonce attribute 'sha256-hash...': Allow resources matching cryptographic hash 'strict-dynamic': Trust scripts loaded by already-trusted scripts
Legacy CSP: Domain Whitelisting (Avoid)
The original CSP approach whitelisted trusted domains:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com https://analytics.example.com
Why This Approach Fails
CDN compromise: If whitelisted CDN is compromised, attackers can serve malicious code JSONP endpoints: Many whitelisted domains offer JSONP endpoints that bypass CSP Maintenance burden: Constantly updating domain list as services change 'unsafe-inline' temptation: Developers often add 'unsafe-inline' to avoid refactoring, negating protection
Research shows domain-based CSP is routinely bypassed and provides limited real protection.
Strict CSP: Nonce-Based Approach (Recommended)
Modern "Strict CSP" uses nonces (numbers used once) to identify legitimate scripts:
How Nonces Work
- Server generates cryptographically random nonce for each request
- Includes nonce in CSP header:
script-src 'nonce-random123' - Adds same nonce to legitimate script tags:
<script nonce="random123"> - Browser executes only scripts with matching nonce
- Injected scripts without valid nonce are blocked
Example Implementation
Server generates nonce:
const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64');
Sets CSP header:
Content-Security-Policy: script-src 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'
Includes nonce in HTML:
<script nonce="r4nd0m">
// Legitimate inline script
console.log('This executes');
</script>
<script src="/app.js" nonce="r4nd0m"></script>
Injected script blocked:
<script>
// Attacker's injected script - no nonce, blocked!
alert('hacked');
</script>
Critical Nonce Requirements
Must be cryptographically random: Use secure random generators, not predictable values Must be unique per request: Different nonce for every page load Minimum 128 bits entropy: 16+ bytes for adequate randomness Never reuse: Nonces lose all security if predictable or reused
Strict CSP: Hash-Based Approach
For static sites or specific inline scripts, hash-based CSP allows scripts matching cryptographic hashes:
How Hashes Work
- Calculate SHA-256/SHA-384/SHA-512 hash of inline script content
- Include hash in CSP:
script-src 'sha256-hash...' - Browser hashes actual script content and compares
- Matching hashes allow execution; mismatches block
Example
Inline script:
<script>
console.log('Hello world');
</script>
Generate hash (using openssl):
echo -n "console.log('Hello world');" | openssl dgst -sha256 -binary | openssl base64
Include in CSP:
Content-Security-Policy: script-src 'sha256-Gt1DvTn4uMT8DT6QLQTYdY+1T8TYQ0t5GE8vIVv6pjQ='
Browser allows this specific script because hash matches.
Hash Limitations
Any change breaks hash: Even adding whitespace changes the hash Manual maintenance: Must recalculate hashes when scripts change Build process integration: Typically automated in build pipelines Best for static content: Dynamic content better served by nonces
strict-dynamic: Trusting Script Dependencies
The 'strict-dynamic' directive trusts scripts loaded by already-trusted scripts:
Content-Security-Policy: script-src 'nonce-random123' 'strict-dynamic'; object-src 'none'; base-uri 'none'
How It Works
Trusted script with nonce:
<script nonce="random123" src="/loader.js"></script>
loader.js dynamically creates script (normally blocked):
const script = document.createElement('script');
script.src = '/dynamic-module.js';
document.body.appendChild(script);
With 'strict-dynamic', the dynamically-created script is trusted because it was created by a trusted script (loader.js with valid nonce).
Benefits
Supports modern JavaScript patterns: Module loaders, code splitting, dynamic imports Reduces CSP maintenance: Don't need nonces on every dynamically-loaded script Backward compatibility: Falls back gracefully in older browsers
CSP Report-Only Mode
Before enforcing CSP, test with report-only mode:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Browsers monitor violations and send reports without blocking resources. This allows:
- Identifying legitimate resources that would be blocked
- Monitoring for injection attempts
- Gradually strengthening policy without breaking functionality
Transition to enforcing mode after confirming policy doesn't break legitimate features.
Complete Strict CSP Example
Recommended CSP for modern applications:
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
report-uri /csp-violations
This policy:
- Uses nonces for script trust (generated per request)
- Enables strict-dynamic for script dependencies
- Blocks plugins (Flash, Java, etc.) with object-src 'none'
- Prevents base tag injection with base-uri 'none'
- Requires Trusted Types (additional XSS protection)
- Reports violations to /csp-violations endpoint
Common Implementation Challenges
Inline Event Handlers
Problem: CSP blocks inline event handlers:
<button onclick="doSomething()">Click</button>
Solution: Move to addEventListener in external/nonce-tagged script:
<button id="myButton">Click</button>
<script nonce="random123">
document.getElementById('myButton').addEventListener('click', doSomething);
</script>
Third-Party Scripts
Problem: Analytics, ads, widgets need script execution
Solution: Load third-party scripts with nonce or use strict-dynamic to allow them to load their dependencies
Style Inline Attributes
Problem: CSP can block inline style attributes
Solution: Use external stylesheets or hash-based style-src
Testing CSP
Validate CSP implementation:
Browser Console: Check for CSP violation messages CSP Evaluator: Google's CSP Evaluator tool Security Headers Analyzer: Our tool Penetration Testing: Attempt XSS payloads to verify blocking
Conclusion
Content Security Policy is the most effective defense against XSS attacks. Modern Strict CSP using nonces or hashes provides robust protection that legacy domain-whitelisting approaches cannot match.
Implement nonce-based CSP for dynamic applications, hash-based for static content. Use 'strict-dynamic' to support modern JavaScript patterns. Start with report-only mode to identify issues before enforcement.
Proper CSP implementation requires initial effort but provides ongoing protection against one of the web's most dangerous vulnerability classes.
Check your CSP implementation with our Security Headers Analyzer for detailed grading and recommendations.
