# I Hope This Sticks: Analyzing ClipboardEvent Listeners for Stored XSS

Zoom Whiteboard deserialized clipboard HTML into protobuf sticky notes rendered via react-contenteditable without sanitization, enabling stored XSS broadcast to all whiteboard viewers.

- Payout: Research
- Severity: high
- Program: Zoom
- Write-up by: Eugene Lim
- Disclosed: 2022-12-17
- Write-up quality: 8/10
- Original: https://spaceraccoon.dev/analyzing-clipboardevent-listeners-stored-xss/
- Library page: https://bugbountylibrary.com/r/blog%3Aspaceraccoon-dev-analyzing-clipboardevent-listeners-stored-xss
- Tags: area:web domain:web impact:integrity kind:research platform:research sev:high tech:websocket technique:incomplete-fix technique:js-analysis technique:source-review vuln:deserialization vuln:xss

**Target:** Web/native Whiteboard client paste handler and WebSocket protobuf rendering
**Root cause:** StickyNote's ContentEditable child omitted sanitizeHTML; convertToText regex /<(.*?)>/g lacks /m flag so tags with newlines (e.g. `<script \n>`) survive, and react-contenteditable passes html to dangerouslySetInnerHTML.
**Steps:**
1. Unpack webpacked Whiteboard client via source maps (Webpack Exploder).
2. Trace paste listener -> prepareData -> read() -> getZDCCopyObjectsFromHtmlString: parses text/html, reads span[data-meta], matches /^<--\(zdc-data\)(.*)\(\/zdc-data\)-->$/, base64+URI-decodes JSON {objs,meta}.
3. Identify StickyNote ContentEditable renders c.content via convertToText (regex strip) instead of sanitizeHTML.
4. Craft objs[0].text with `<iframe srcdoc='<script>alert()</script>' \n></iframe\n>` to bypass /<(.*?)>/g.
5. Build HTML `<meta charset="utf-8"><span data-meta="<--(zdc-data)BASE64(URI(JSON))( /zdc-data)-->"></span>` and write to clipboard via navigator.clipboard.write([new ClipboardItem({'text/html': blob})]).
6. Paste into Whiteboard; client sends protobuf over WebSocket to server, broadcast to all viewers, rendered unsanitized -> XSS.
**How found:** Source-map unpacking plus CodeQL skim surfaced DataTransfer.getData(text/html) paste handler; traced deserialization into protobuf; noticed `<b>` worked in sticky notes despite no dangerouslySetInnerHTML, leading to react-contenteditable dependency; found missing sanitizeHTML on StickyNote and regex bypass via newline.
**Exploitation:** Attacker places crafted text/html clipboard blob, pastes into a shared Whiteboard; the deserialized sticky note protobuf is sent to the server and rebroadcast to every collaborator, executing script in their session (stored XSS).
**Escalation:** Stored/broadcast nature turns self-XSS into stored XSS affecting all whiteboard viewers.
**Insight:** When a sanitizer regex uses /g without /m, tags containing newlines (e.g. `<script \n>`) slip through — test multiline payloads against regex-based HTML strippers. Also audit react-contenteditable children for missing DOMPurify since it forwards html to dangerouslySetInnerHTML.
