Last updated: April 26, 2026
Content Security Policy (CSP) is the modern XSS defence — restricts where scripts can load from. Done well, CSP makes XSS exploitation difficult; done badly, it gives a false sense of security. This article covers CSP bypass techniques and the configurations that close them.
The CSP basics
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'
This says scripts can load from the same origin or jsdelivr.net. Inline styles are allowed (unsafe-inline). Anything else blocked.
Common bypasses
1. JSONP endpoints on trusted CDN
# Site CSP allows scripts from cdn.jsdelivr.net
# Attacker injects HTML:
<script src="https://cdn.jsdelivr.net/some/jsonp?callback=alert(document.cookie)//"></script>
# JSONP response: alert(document.cookie)//(...)
# Executes attacker's code from a CSP-trusted origin
2. AngularJS or similar templating libraries on trusted origin
# If CDN hosts AngularJS and CSP allows it:
<div ng-app>{{constructor.constructor('alert(1)')()}}</div>
# AngularJS sandbox bypass executes arbitrary JS
3. unsafe-inline / unsafe-eval allowed
Defeats CSP’s primary purpose. Often present in legacy applications transitioning to CSP.
4. Wildcard sources
# Bad: script-src https://*.googleusercontent.com
# Attacker uploads malicious content to user-content.googleusercontent.com
# CSP allows it
5. data: / blob: URIs in script-src
# Bad: script-src 'self' data:
# Attacker injects:
<script src="data:application/javascript,alert(1)"></script>
# CSP allows data: → bypass
6. Path-based bypasses
# CSP: script-src https://target.com/static/
# Attacker finds path-traversal accessible from /static/:
<script src="https://target.com/static/../upload/evil.js"></script>
# Browser doesn't normalise; CSP allows; attacker JS loads
Modern CSP — the safe defaults
Content-Security-Policy:
default-src 'none';
script-src 'self' 'nonce-RANDOM_PER_REQUEST' 'strict-dynamic';
style-src 'self' 'nonce-RANDOM_PER_REQUEST';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.target.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
require-trusted-types-for 'script';
trusted-types default;
report-uri /csp-report
Key elements:
- nonce-based — only inline scripts with the matching nonce execute. Generated per-request, unguessable
- ‘strict-dynamic’ — trust scripts loaded by trusted scripts; eliminates need to manage CDN allow-lists
- ‘unsafe-inline’ fallback — older browsers ignore nonce; including ‘unsafe-inline’ as fallback for them is acceptable since modern browsers ignore it when nonce is present
- frame-ancestors ‘none’ — clickjacking defence
- base-uri ‘self’ — prevents base-tag injection attacks
- require-trusted-types-for ‘script’ — DOM-XSS defence (covered separately)
- report-uri — collect violation reports for monitoring
Deployment workflow
- Start with
Content-Security-Policy-Report-Onlyheader — log violations, don’t enforce - Collect 30-60 days of reports; identify legitimate sources
- Move to enforcement (
Content-Security-Policyheader) - Iterate as new features add new sources
Detection — CSP violation reports
POST /csp-report
Content-Type: application/csp-report
{
"csp-report": {
"document-uri": "https://target.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://attacker.com/evil.js",
"source-file": "https://target.com/page",
"line-number": 42
}
}
Aggregate and alert on suspicious sources. Multiple reports of the same blocked-uri across many users = active XSS attempt.
The takeaway
CSP done well (nonce-based + strict-dynamic + Trusted Types) makes XSS exploitation hard. CSP done badly (wildcards, unsafe-inline) provides false comfort. Audit your current CSP against the safe defaults; deploy in report-only first; iterate to enforcement.
Get a VAPT scoping call
Senior practitioner-led VAPT — not a checklist run by juniors. CVSS-scored findings, free retest, attestation letter. India's SMBs and SaaS teams.