What if a single missing function call – one middleware reference, 27 characters – could give any attacker on your network complete control over your nginx web server? No credentials needed. No exploitation complexity. Just a plain HTTP request to a URL that should have been protected but wasn’t.

That’s CVE-2026-33032, a critical vulnerability (CVSS 9.8) we discovered in nginx-ui, a popular web-based nginx management tool with over 11K GitHub stars and 430,000+ Docker pulls. Since publication, active exploitation in the wild has been confirmed: VulnCheck added it to their Known Exploited Vulnerabilities (KEV) list, and Recorded Future’s Insikt Group identified it as one of 31 high-impact CVEs actively exploited in March 2026, assigning it a Risk Score of 94/100 alongside vulnerabilities in Cisco, Microsoft, Google, and Citrix.

TL;DR: The MCP message endpoint (/mcp_message) in nginx-ui is missing authentication middleware. This exposes 12 MCP tools – including config writes with automatic nginx reload – to any host on the network. One unauthenticated API call is all it takes to inject a config and take over nginx.

Attacker-injected page served by nginx after exploiting CVE-2026-33032
The end result: an attacker-injected page served directly by nginx, created and activated via a single unauthenticated MCP tool call.

The Growing Attack Surface of MCP

This is the second post in our series on MCP security. In the first installment, we uncovered MCPwnfluence – a critical SSRF-to-RCE chain in the most widely deployed Atlassian MCP server – demonstrating how MCP servers act as privileged bridges between AI agents and critical infrastructure, and how a single flaw in that bridge can compromise everything behind it.

CVE-2026-33032 takes that theme further. Where MCPwnfluence showed how an MCP server’s own vulnerabilities can be chained for RCE, this finding reveals something more fundamental: when you bolt MCP onto an existing application, the MCP endpoints inherit the application’s full capabilities but not necessarily its security controls. The result is a backdoor that bypasses every authentication mechanism the application was carefully built with.

Both findings are part of a broader research effort mapping the risk landscape of the MCP ecosystem – an ecosystem growing fast, with hundreds of new MCP servers appearing every week, many of them wrapping critical infrastructure with varying degrees of security rigor.

nginx-ui: Where AI Meets YourWeb Server

nginx-ui is exactly what it sounds like – a web interface for managing nginx. Edit configs, manage SSL certificates, monitor status, restart the server. Over 10,600 GitHub stars worth of developers chose it over SSHing into boxes and hand-editing config files.

Its Docker image (uozi/nginx-ui) has been pulled over 430,000 times – meaning hundreds of thousands of nginx instances could be managed through this tool, each one sitting in front of production traffic, APIs, and internal services.

And it’s not just a development tool – nginx-ui has a significant public footprint. Using [Shodan], we searched for nginx-ui’s favicon hash (`http.favicon.hash:-1565173320`), a technique that identifies specific web applications by the unique fingerprint of their icon file. The result: **over 2,600 publicly exposed nginx-ui instances**, spread across cloud providers like Alibaba Cloud, Oracle, Tencent, and DigitalOcean, with the majority of instances on port 9000 (the default nginx-ui backend port).

[Shodan search results showing 2,689 publicly exposed nginx-ui instances]
*Shodan results for nginx-ui’s favicon hash: 2,689 publicly exposed instances across 50+ countries. Each one is a potential target for CVE-2026-33032.*

Β 

Like many modern applications, nginx-ui recently added MCP support. The promise: ask your AI assistant to “add a reverse proxy for my new API” and it creates the nginx configuration directly. Powerful. Convenient. And, as we’ll see, potentially devastating when the MCP integration doesn’t carry over the application’s existing security model.

Because MCP support means exposing your application’s most sensitive operations – config writes, service restarts, file reads – through a new set of HTTP endpoints. If those endpoints aren’t protected with the same rigor as the rest of the application, they don’t just become a weakness. They become the weakness.

Under the Hood: Two Endpoints, One Problem

How the MCP Transport Works

nginx-ui uses the SSE (Server-Sent Events) transport from the mcp-go library, which splits communication across two HTTP endpoints:

  • GET /mcp – Opens a persistent SSE stream. The client connects here to receive responses. Think of this as the “listening” channel.
  • POST /mcp_message – Receives JSON-RPC tool invocations. Every config write, every nginx restart, every file read goes through here. This is the “action” channel.

When a client connects, it GETs /mcp to open the stream. The server responds with a session ID:

event: endpoint
data: /mcp_message?sessionId=4f4cdb82-152b-4c10-8f63-1df90e1e061f

From that point on, tool calls are POSTed to /mcp_message?sessionId=xxx, and responses flow back through the SSE stream. nginx-ui authenticates MCP connections using a node_secret query parameter, as documented in their MCP guide.

Spot the Difference

Here’s the MCP router in its entirety. Read it carefully.

// mcp/router.go (vulnerable version - v2.3.3 and earlier)
func InitRouter(r *gin.Engine) {
    r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(),
        func(c *gin.Context) {
            mcp.ServeHTTP(c)
        })
    r.Any("/mcp_message", middleware.IPWhiteList(),
        func(c *gin.Context) {
            mcp.ServeHTTP(c)
        })
}

/mcp has IPWhiteList() and AuthRequired().

/mcp_message has IPWhiteList() only.

Both route to the exact same handler. But /mcp_message – the endpoint where every destructive operation happens – skips authentication entirely.

The authentication gap between nginx-ui's two MCP endpoints
The authentication gap: both endpoints reach the same handler, but only one checks credentials.

The Safety Net That Isn’t

The IP whitelist is the only remaining protection. But it has a fatal default:

// internal/middleware/ip_whitelist.go
if len(settings.AuthSettings.IPWhiteList) == 0 {
    c.Next()  // Empty whitelist = allow everyone
    return
}

Empty whitelist. Fail-open behavior. Every fresh installation allows all IPs through. Two security mechanisms, both fail. The result: any host on the network can invoke any MCP tool, including ones that rewrite your nginx configuration and reload the server.

12 Tools, Zero Authentication

What exactly does an unauthenticated attacker get access to? Everything.

7 destructive tools that can modify your nginx:

Tool Capability
nginx_config_add Create config files + auto-reload nginx
nginx_config_modify Modify any existing config
nginx_config_enable Enable/disable sites
nginx_config_rename Rename config files
nginx_config_mkdir Create directories
reload_nginx Reload configuration
restart_nginx Restart the process

5 read-only tools for reconnaissance:

Tool Capability
nginx_config_get Read any config file
nginx_config_list Enumerate all configurations
nginx_config_base_path Get config directory path
nginx_config_history View config change history
nginx_status Read server status

Pay special attention to nginx_config_add – it automatically reloads nginx after writing the config. Config injection and activation in a single, unauthenticated API call.

The Attack: Network Access to Full Takeover in Seconds

The cross-network attack flow for CVE-2026-33032
The attack flow: step 1 authenticates to get a session, step 2 uses that session to invoke destructive tools with zero authentication.

An attacker on the same network as the nginx-ui instance needs just two requests:

  1. GET /mcp?node_secret=xxx – Establish an SSE session, get a sessionId
  2. POST /mcp_message?sessionId=xxx – Invoke any tool. No node_secret. No JWT. No cookies. Nothing.

From that single unauthenticated request, the attacker can:

Intercept all traffic – Rewrite server blocks to proxy everything through an attacker-controlled endpoint. Every request, every response, every credential – captured.

Harvest admin credentials – Inject access_log directives with custom log_format patterns that capture Authorization headers from administrators accessing nginx-ui.

Escalate to permanent access – With a captured admin JWT, access the nginx-ui settings API to extract the JwtSecret. Forge admin tokens for any user. Maintain persistent access that survives config cleanup.

Exfiltrate the architecture – Read every nginx config file, mapping backend topology, upstream servers, TLS paths, and internal service addresses.

Kill the service – Write an invalid config, trigger a reload. nginx goes down, and everything behind it goes with it.

Seeing Is Believing

We built a PoC that demonstrates the full attack from a separate machine on the network – not localhost, not the same container, but a completely different host with its own IP address.

The attacker container (172.21.0.3) connects to the nginx-ui target (172.21.0.2), enumerates all 12 tools, exfiltrates the nginx configuration, and injects a new server block – all without any authentication:

╔═══════════════════════════════════════════════════════════════╗
β•‘              ATTACKER MACHINE                                β•‘
╠═══════════════════════════════════════════════════════════════╣
β•‘  Hostname:  29c4a25ad0ae                                     β•‘
β•‘  IP:        172.21.0.3                                       β•‘
β•‘  Target:    http://172.21.0.2:9000                           β•‘
β•‘  Auth sent: NONE                                             β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

β†’ POST http://172.21.0.2:9000/mcp_message?sessionId=e336a0ed-...
β†’ From attacker IP: 172.21.0.3
β†’ Authorization: *** NONE ***

βœ“ 12 tools accessible from attacker machine
    ⚠  nginx_config_add (DESTRUCTIVE)
    ⚠  nginx_config_modify (DESTRUCTIVE)
    ⚠  restart_nginx (DESTRUCTIVE)
    ...

βœ“ nginx.conf exfiltrated from attacker machine!
βœ“ Config injected from attacker machine!
βœ“ Nginx auto-reloaded on target

The injected config creates a new server block that immediately begins serving attacker-controlled content – confirming write + reload, all from a remote machine with zero credentials.

This is what the end-to-end exploitation flow looks like – from establishing the SSE session through tool enumeration, config exfiltration, and finally injecting a malicious server block that nginx loads immediately:

CVE-2026-33032 PoC Demo
Full exploitation: the PoC connects from a separate machine, enumerates 12 unauthenticated tools, reads nginx.conf, injects a malicious config, and nginx auto-reloads – all without any credentials.

Dissecting the Fix

The vulnerability was fixed in v2.3.4 (released March 15, 2026, one day after our report). The fix is exactly what you’d expect – adding the missing AuthRequired() middleware:

r.Any("/mcp_message", middleware.IPWhiteList(),
+ r.Any("/mcp_message", middleware.IPWhiteList(),
middleware.AuthRequired(),
func(c *gin.Context) {
    mcp.ServeHTTP(c)
})

One line. 27 characters added: , middleware.AuthRequired(). The same middleware that /mcp already had.

What’s notable is what came alongside the fix in commit 413dc63: the maintainers also added a regression test (mcp/router_test.go) that explicitly verifies both MCP endpoints return 403 when accessed without authentication:

func TestMCPEndpointsRequireAuthentication(t *testing.T) {
    settings.AuthSettings.IPWhiteList = nil

    router := gin.New()
    InitRouter(router)

    for _, endpoint := range []string{"/mcp", "/mcp_message"} {
        req := httptest.NewRequest(http.MethodPost, endpoint, nil)
        w := httptest.NewRecorder()
        router.ServeHTTP(w, req)

        assert.Equal(t, http.StatusForbidden, w.Code)
    }
}

This test would have caught the original vulnerability. Its absence is a reminder that security-critical middleware chains need explicit test coverage – especially when a protocol uses multiple endpoints.

A note on version tracking: The OSV entry lists v2.3.5 as the last affected version, while the GHSA lists <=1.99. Both appear to be incorrect. Based on our source code verification: v2.3.3 is the last vulnerable version, and v2.3.4 contains the fix.

The Pattern: MCP as the Weakest Link

This vulnerability is not an isolated case. It’s a pattern we keep encountering across the MCP ecosystem.

When developers add MCP to an existing application, they’re exposing the app’s most powerful operations through new HTTP endpoints. The web API is authenticated. The WebSocket connections are authenticated. But the MCP endpoints? Those are new, and it’s easy to miss one.

The SSE transport makes this especially tricky because it uses two endpoints. Developers intuitively think of the SSE stream as the “connection” that needs protecting, and the message endpoint as just a data pipe. But in MCP’s architecture, the message endpoint is where the power lives. Protecting the stream but not the message endpoint is like locking the front door while leaving the back door wide open.

We’ve seen variations of this across multiple MCP server implementations:
– Authentication on the SSE endpoint but not the message endpoint
– IP whitelists that default to allow-all
– Security flags documented in READMEs but never checked in code
– OAuth scopes advertised but never enforced

The takeaway for anyone integrating MCP into an existing application: every endpoint in the MCP transport must inherit your full authentication stack. The message endpoint processes all tool invocations, including destructive ones. There is no passive MCP endpoint.

What to Do

Active exploitation of CVE-2026-33032 has been confirmed in the wild. If you are running a vulnerable version, update immediately – do not wait.

If you’re running nginx-ui with MCP enabled:
– Update to v2.3.4 or later immediately
– If you cannot update right now, disable MCP entirely or lock the IP whitelist to trusted hosts as an emergency stopgap – do not leave the default fail-open configuration in place
– Review nginx access logs for unexpected configuration changes
– Check conf.d/ and sites-enabled/ for unfamiliar config files

If you’re building MCP integrations:
– Audit both SSE endpoints – the stream and the message handler
– Test authentication on the message endpoint explicitly (the nginx-ui fix includes a great test template)
– Default fail-closed on IP allowlists
– Treat MCP endpoints with the same security rigor as your most privileged API

Timeline

Date Event
2026-03-04 Vulnerability discovered during MCP ecosystem research
2026-03-04 Reported via GitHub Private Vulnerability Reporting
2026-03-14 Fix committed (413dc63)
2026-03-15 v2.3.4 released with fix
2026-03-28 CVE-2026-33032 published
2026-03 CVE-2026-33032 actively exploited in the wild (per Recorded Future Insikt Group March 2026 CVE Landscape report)
2026-04-13 VulnCheck adds CVE-2026-33032 to its Known Exploited Vulnerabilities (KEV) list; active exploitation publicly confirmed

What’s Next

This is the second in a series of posts from our MCP security research. We’ve identified and responsibly disclosed vulnerabilities across multiple popular MCP server implementations – from official MCP servers to community projects with thousands of stars. Follow along to stay informed as we release further findings.