A comprehensive guide to understanding Content Security Policy from a penetration tester's perspective, revealing common misconfigurations and bypass techniques that security professionals encounter in real-world assessments.
A few weeks ago, I was knee-deep in a CTF challenge. Found an XSS vulnerability, felt good about it, crafted my payload, and… nothing. The page just sat there, mocking me. Turns out the Content Security Policy (CSP) was configured in a very specific way that blocked everything I tried. After spending an hour actually reading the policy line by line, understanding what was allowed and what wasn't, I eventually got it. But that experience made me realize how little attention many pentesters pay to this crucial security header.
What CSP Actually Is
Picture this: you're running a nightclub. You don't want random people wandering in off the street, so you hire a bouncer. That bouncer has a list, and if you're not on it, you're not getting in. CSP is essentially that bouncer, but for your browser.
The server sends a policy to the browser saying "hey, only execute scripts from these specific places I trust." When you try to inject malicious code from somewhere else, the browser goes "nope, not on the list" and blocks it.
Here's what the flow looks like:
Server: "Content-Security-Policy: script-src 'self'"
You:
Click
All of these are inline. A proper CSP blocks them unless 'unsafe-inline' is present. But here's the thing: tons of legacy applications have inline scripts scattered everywhere. Refactoring all of that is a massive undertaking, so devs take the easy way out. They slap 'unsafe-inline' in there "temporarily" and call it a day. I've seen "temporary" fixes that have been in production for three years.
'unsafe-eval' is similar but for a different type of code execution. It allows functions like eval() that take strings and execute them as code:
eval('alert(1)'); setTimeout('alert(1)', 0); new Function('alert(1)')();
If you can control what string gets passed to any of these functions, and 'unsafe-eval' is present, you're in.
Then there are the wildcards. * means any domain. https: means any HTTPS site (which is basically everything now). data: allows data URIs, so you can embed code directly in a URL. *.example.com allows any subdomain. All of these are red flags because they're way too permissive.
Where Things Break Down
Let me show you the misconfigurations I see over and over again in real assessments.
The most common one? 'unsafe-inline' just sitting there in the policy:
Content-Security-Policy: script-src 'self' 'unsafe-inline'
When you see this, your standard XSS payloads work perfectly:
Next up is the missing base-uri. Check this out:
Content-Security-Policy: script-src 'self'
Looks pretty locked down, right? Script source is restricted to the same origin. But there's no base-uri directive. That means you can inject a tag:
Now when the page loads its legitimate scripts:
The browser goes "okay, base is attacker.com, so this must be https://attacker.com/js/app.js" and loads your malicious script instead. You didn't even need to inject your own script tag. You just redirected theirs.
Then there's the lazy wildcard approach:
Content-Security-Policy: script-src 'self' https:
The https: directive allows any HTTPS site. Since 99% of the internet runs on HTTPS now, this is basically worthless:
Just works. Same story with data: URLs:
Subdomain wildcards are another fun one:
Content-Security-Policy: script-src 'self' *.example.com
All you need is ONE vulnerable subdomain. Could be an old forgotten staging server, could be a user upload feature on uploads.example.com, doesn't matter. Find one weakness in any subdomain and the entire CSP falls apart:
Last one: missing object-src. When this directive isn't specified, you can sometimes use or tags to bypass everything. It's browser-dependent and a bit finicky, but it works often enough that it's worth checking.
Finding CSP in the Wild
Most of the time you'll be using Burp Suite or another proxy to intercept traffic. Just look at the response headers in the HTTP history and search for Content-Security-Policy. That's honestly the most practical way when you're doing actual testing.
Quick curl command gets you started:
curl -I https://example.com
Or just pop open DevTools (F12), go to the Network tab, reload, click the main request, and look at Response Headers.
Sometimes it's in a meta tag instead:
One thing to remember: CSP can be in both the header and a meta tag. When that happens, the most restrictive one wins. I've seen cases where the header was solid but the meta tag had 'unsafe-inline', and guess which one applied? The restrictive one. But I've also seen the opposite, where the header was weak and the meta tag tried to lock things down, and the weak header took precedence. Point is, check both.
Quick Analysis Approach
When you find a CSP, here's what I do:
First, I look for the obvious wins. Search for 'unsafe-inline'. If it's there, I can probably stop looking and just fire off my XSS payload. Search for 'unsafe-eval' too, because if the app uses any eval-style functions, that's another easy win.
Check for wildcards: *, https:, data:. These are all way too permissive and usually mean the policy isn't doing much.
Then I verify what's missing. Is base-uri there? If not, can I inject HTML? If yes to both, base tag injection might work. Is object-src there? If not, object/embed tags are worth trying.
Look for subdomain wildcards. If you see *.example.com, time to enumerate subdomains and look for vulnerable ones or file upload functionality.
There's a tool from Google called CSP Evaluator that automates a lot of this analysis. Paste in the policy and it'll tell you what's weak. Super useful for quick assessments.
Wrapping Up
So that's the foundation. What CSP is, how it works, the directives that matter, and the misconfigurations you'll run into constantly. The reality is that most CSPs have at least one weakness, usually because getting this right is genuinely difficult.
It's not that developers are bad at their jobs. It's that CSP is complex, and the tradeoffs between security and functionality are real. Legacy applications with inline scripts everywhere, third-party integrations that require specific domains, the need for certain browser features—all of these create pressure to weaken the policy.
As pentesters, understanding these nuances gives us an edge. We can spot the misconfigurations that others miss, whether it's a missing base-uri directive or an overly permissive wildcard. The next time you encounter a CSP, don't just give up when your first payload fails. Take a closer look. There's almost always a way through.

Comments
Please log in or register to join the discussion