# Stored XSS in Post Preview as Contributor

Contributor-authored post preview runs preg_replace_callback after esc_url sanitization, converting %u003A back to a colon and reviving a javascript: link for stored XSS against admins.

- Payout: hidden
- Severity: medium
- Program: WordPress
- Found by: simonscannell
- Disclosed: 2020-08-18
- Write-up quality: 7/10
- Original: https://hackerone.com/reports/497724
- Library page: https://bugbountylibrary.com/r/h1%3A497724
- Tags: area:web domain:web impact:ato kind:bounty platform:hackerone sev:medium tech:php technique:incomplete-fix technique:source-review vuln:misconfiguration vuln:xss

**Target:** WordPress post preview / get_the_content()
**Root cause:** get_the_content() applies preg_replace_callback('/\%u([0-9A-F]{4})/', '_convert_urlencoded_to_entities') to the already-sanitized output during preview, decoding %uXXXX sequences into HTML entities after esc_url() has run, so a javascript: scheme hidden as javascript%u003A survives sanitization and is restored to javascript: in the browser.
**Steps:**
1. As a Contributor, create a new post.
2. Insert HTML: `<a href="javascript%u003Aalert(/XSS/)">text</a>`
3. Click Preview post.
4. Click the rendered link; the browser decodes the entity to `javascript:` and executes alert(/XSS/) in the admin's session.
**How found:** Code reading of get_the_content() revealed the preview-only preg_replace_callback runs after all sanitization; the researcher hypothesized that %uXXXX decoding into HTML entities would undo esc_url()'s scheme filtering, then tested the javascript%u003A payload in Firefox preview.
**Exploitation:** Attacker with Contributor role submits a post containing the crafted anchor, then lures an admin to preview it; clicking the link executes JS in the admin context. The report notes CSS classes can overlay the link invisibly across the page to increase click likelihood.
**Escalation:** Chained with existing CSS classes to render the malicious link as an invisible full-page overlay, raising click probability.
**Insight:** Look for decode/normalize passes that run after sanitizers (here a preview-only %uXXXX-to-entity conversion after esc_url). Any post-sanitization transformation that reintroduces scheme characters (colon, etc.) can revive javascript: URLs; test %uXXXX, entity, and percent-encoded variants specifically in preview/draft rendering paths.

