PortSwigger

Blind trust: stealing credentials through CSP


  • Published: Wednesday, 11 November 2026 at 14:26 UTC

  • Updated: Wednesday, 11 November 2026 at 14:26 UTC

A picture showing the Burp AT logo with an arrow going round CSP

Content Security Policy is supposed to be the safety net that stops an HTML injection from turning into XSS. Usually it works. But sometimes it doesn’t, because the policy allows-lists a domain that hosts a JSONP endpoint or an old copy of AngularJS, or because whoever wrote the policy forgot that one directive doesn’t inherit from default-src.

Spotting those mistakes by hand is tedious. You read the header, work out which directive actually applies, check every allowed host against the gadgets you remember, then build a proof of concept. So we turned that process into a Burp AT skill. Tell Burp AT you want to bypass a CSP and it reads the policy, finds the weaknesses and builds you a PoC that exfiltrates to Burp Collaborator.

What the skill does

The skill runs two checks against the Content-Security-Policy on a response:

  • 1. Allow-list gadget lookup. Are any of the allowed script sources known to host a CSP bypass gadget?

  • 2. Form-action credential exfiltration. Does the policy let an injected form post back to the page, so credentials can be stolen even when no JavaScript runs at all?

1. Hunting for gadgets in the allow-list

A host allow-list is only as strong as the weakest file on any host in it. If script-src allows a domain that serves a JSONP endpoint with a controllable callback, an attacker can load that endpoint as a script and run arbitrary JavaScript from an allowed origin.

The skill comes with a dataset of more than 350 known gadgets provided by
cspbypass.com big thanks to them, each a domain plus a working snippet. To match against it, the script:

  • Works out the effective script sources. It follows the browser’s fallback chain: script-src-elem, then script-src, then default-src. If none of those are present, scripts are unrestricted.

  • Matches hosts the way the browser does. A source can match exactly, or as a *.example.com wildcard that covers subdomains. A bare * or a scheme-only source like https: allows any host, and the report says so.

  • Intersects multiple policies. When a response sends more than one enforced CSP header, the browser has to satisfy all of them, so a gadget is only reported if every policy allows its domain.

  • Handles report-only. If the only header is Content-Security-Policy-Report-Only, the skill still analyses it but marks the findings as informational, because that header blocks nothing.

  • Flags caveats that change the answer. With strict-dynamic, modern browsers ignore the host allow-list, so host gadgets won’t fire. A nonce or hash may gate execution. And if unsafe-inline appears without a nonce, hash or strict-dynamic, you don’t need a gadget at all.

Each matching domain comes back with its gadget snippet, along with the effective sources for each policy and any caveats.

2. Stealing credentials with a self-posting form

The second check covers the case that’s easy to miss. Even when policies block all JavaScript with default-src 'none' there is still a way to steal data.

form-action does not fall back to default-src. If a policy leaves out form-action, forms can submit anywhere. If it sets form-action 'self', forms can still submit to the page itself, and that’s all this attack needs.

When the page has a reflected HTML-injection sink, the skill builds a two-stage form:

?targetParam=

Here’s how it works:

  • 1. The form is injected through the vulnerable parameter ($targetParam). It has no action, so it submits a GET request back to the same page, which form-action 'self' allows.

  • 2. The