A single missing function call earned a CVSS 9.8. In March 2026, researchers at Pluto Security found that nginx-ui — a popular open-source web UI for managing nginx — had shipped a Model Context Protocol endpoint that never called its own AuthRequired() check. One unauthenticated HTTP request could rewrite nginx configs, reload the server, and take it over completely. Shodan data put roughly 2,689 instances on the public internet, most of them reachable on the default port 9000, and within weeks the flaw was under active exploitation in the wild.
Here is the short version, with the receipts below: the bug was mundane — an endpoint added for a new feature, wired for speed, authenticated "when someone remembers" — but the blast radius was an admin tool that already holds full nginx and TLS config and, on many self-hosted stacks, sits one hop from every other service on the box. The lesson generalizes to every team bolting an MCP server onto something with write access: every new MCP surface is an unauthenticated-by-default risk until proven otherwise. This post tells the MCPwn story precisely, then turns it into a concrete audit checklist for your own MCP routes.
MCPwn in 60 seconds: what happened
nginx-ui (0xJacky/nginx-ui) added Model Context Protocol support so AI agents could manage nginx through tools instead of SSH and config files. That integration exposed two HTTP endpoints, and only one of them was authenticated. Pluto Security, an AI-workflow security company, found the gap, disclosed it responsibly, and codenamed it MCPwn. The timeline from report to mass exploitation ran about a month:
| Date | Event |
|---|---|
| Mar 14, 2026 | Pluto Security reports the flaw to the nginx-ui maintainers |
| Mar 15, 2026 | Fixed in nginx-ui 2.3.4, released one day after the report |
| Late Mar 2026 | CVE-2026-33032 assigned (CVSS 9.8); technical details and a public proof-of-concept emerge |
| Mar 2026 | Recorded Future's Insikt Group flags it as one of 31 high-impact vulnerabilities actively exploited that month, with a risk score of 94 out of 100 |
| Apr 13, 2026 | VulnCheck adds it to its Known Exploited Vulnerabilities list; CSA Singapore issues alert AL-2026-039 |
The exposure numbers explain the urgency. Pluto's Shodan-based scans found approximately 2,600 publicly reachable nginx-ui instances, with Shodan data cited by press reports putting the figure at about 2,689 — concentrated in China, the United States, Indonesia, Germany, and Hong Kong, spread across Alibaba Cloud, Oracle, Tencent, and others, nearly all on port 9000. As OWASP's AI security verification research later noted, this was effectively the first actively exploited MCP CVE — the moment "MCP security" stopped being a conference topic and became an incident category. The incident was covered by BleepingComputer, The Hacker News, Dark Reading, Infosecurity Magazine, and SecurityWeek.
The bug, concretely: two routes, one missing call
This is the entire vulnerability, as reconstructed from the advisory and a public vuln-lab reproduction. In mcp/router.go, the two MCP endpoints were registered like this:
r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(), ...)
r.Any("/mcp_message", middleware.IPWhiteList(), ...) // ❌ MISSING AuthRequired()/mcp establishes the connection: it required both IP whitelisting and authentication. /mcp_message processes every tool invocation — the endpoint that actually does things — and it applied only the IP whitelist. Worse, the default IP whitelist was empty, and the middleware treated empty as "allow all." So the one gate on the dangerous endpoint defaulted to open.
The attack flow, per Pluto Security, needed only network access:
- Retrieve the node secret, then establish an SSE connection and open an MCP session on
/mcp. - Take the returned session ID and POST tool invocations to
/mcp_message— no user credentials, noAuthorizationheader. - Invoke any of the 12 exposed MCP tools: restart nginx, create, modify, or delete configuration files, trigger automatic config reloads, write arbitrary files.
Step 3 is where "auth bypass" becomes "server takeover." Config injection plus an automatic reload means an attacker can reroute traffic, terminate TLS with their own logic in the middle, or simply own the box — as Pluto's Yotam Perkal put it, "one unauthenticated API call is all it takes to inject a config and take over nginx." Note the asymmetry the timeline reveals: the maintainers patched in a day, but the patch only helps teams that apply it, and internet-wide scanning found thousands of instances still reachable weeks later. The median self-hosted admin panel is not patched in a day; it is patched when someone remembers.
Why MCP surfaces fail this way
MCPwn is worth more than a patch-and-move-on because the failure pattern is structural. Four properties of MCP integrations conspire to produce exactly this bug:
Tools are privileged actions by design. An MCP server is not a read-only API with a couple of write endpoints sprinkled in — its whole purpose is letting an agent do things: run commands, edit configs, restart services. Every tool is a capability, so an authentication gap on the tool-invocation path is automatically a privileged-action gap. There is no "harmless" unauthenticated MCP tool on an admin surface.
Frameworks auto-register routes teams forget. MCP SDKs and framework integrations register protocol routes (/mcp, /mcp_message, SSE streams, well-known metadata) on the developer's behalf. The team remembers wiring auth onto the routes they wrote; the framework's routes inherit whatever the default middleware stack happens to be. nginx-ui's team clearly knew how to protect an endpoint — /mcp had both gates. They just never applied the same treatment to the second route, the one that mattered more.
Session-based transports hide the second endpoint. The SSE transport splits "connect" from "invoke": you authenticate (or obtain a session) over here, then send messages over there. That split makes it psychologically easy to guard the front door while leaving the message channel open — the session ID feels like a credential, even when obtaining one requires no authentication at all.
Fail-open defaults ship. "Empty allowlist means allow all" is the kind of default that makes demos work and incidents happen. It is the IP-filtering equivalent of running as root because permissions were annoying during development. Any default that silently converts "unconfigured" into "unrestricted" will eventually meet a production deployment where nobody configured it — roughly 2,689 of them, in this case.
The audit checklist this incident adds
Here is the practical payoff: a seven-item checklist for any team exposing an MCP server, each item tied to the specific MCPwn failure it would have caught. Run it before your MCP surface reaches production, and re-run it every time you upgrade the framework.
- Enumerate every route, including framework-registered ones. Dump the actual route table from the running server — not the routes you remember writing. MCPwn's forgotten route was
/mcp_message, registered by the MCP integration, not by hand. If your framework offers a route-listing or debug endpoint, use it; if not, log the table at startup. Catches: the auto-registered route nobody audited. - Assert authentication on every route with a test, not a review. Write an automated test that hits each route without credentials and expects 401/403. Code review caught nothing here — the missing
AuthRequired()sat in plain sight next to a correct registration. A five-line table-driven test ("every route in this list rejects anonymous requests") would have failed the build. Catches: the missing middleware call. - Make every access-control default deny, and prove it. Empty allowlist must mean "allow none," never "allow all." After MCPwn, the correct default is flipped: start closed, require explicit configuration to open, and fail startup — loudly — if the auth configuration is absent rather than running open. Catches: the fail-open whitelist.
- Don't hang admin MCP ports on the public internet. Thousands of nginx-ui instances sat directly on port 9000. Bind admin MCP listeners to localhost or an admin network, put them behind your existing authenticated ingress, and treat "reachable from Shodan" as a finding in itself. Network posture is defense in depth, not the primary control — but it buys time when the primary control has a bug. Catches: the blast-radius multiplier.
- Scope tools to least privilege, individually. Twelve tools with full config-write and restart power behind one gate means one gate failure is total compromise. Give destructive tools (config write, reload, restart, file write) their own scopes or confirmation requirements so a single bypass degrades instead of detonates. The MCP authorization spec's per-tool scopes exist for exactly this. Catches: the all-or-nothing tool bundle.
- Treat session and node secrets as credentials with a lifecycle. The attack needed a session ID plus a node secret — both obtainable without user authentication. Session identifiers must be unguessable, short-lived, bound to the authenticated principal that created them, and useless without that principal's credential alongside. Catches: the session-ID-as-auth confusion.
- Log every tool invocation with identity attached. Config rewrites and restarts should produce audit lines saying who (which principal, which token) invoked what with which arguments. nginx-ui operators were told to review nginx logs for unexpected config changes — after the fact, by hand. Structured tool-call audit logs turn that archaeology into an alert. Catches: the silent exploitation window.
Items 1–3 are the ones that would have prevented MCPwn outright; items 4–7 are what contain the next one. The whole list fits on an index card, which is the point — the bug fit on one line.
What this means for a deploy API's MCP server
Now scale the stakes up. nginx-ui guards one server's nginx configs. A platform that adds an MCP server to its deploy API hands agents write access to tenant infrastructure: create apps, push config, rotate secrets, scale fleets. The same one-line bug there doesn't compromise a web server — it compromises every tenant the platform hosts, across the trust boundary the whole business depends on.
Three things must therefore be different on a deploy API's MCP surface:
- OAuth 2.1, not IP allowlists. The MCP specification has mandated OAuth 2.1 as the authorization standard for HTTP-based MCP servers since its March 2025 update — treat every MCP client as an OAuth client and the server as a resource server, with protected-resource metadata. Microsoft's field notes on building a secure MCP server with OAuth 2.1 show the pattern working in production. IP whitelisting is a network control; it is not identity, and after MCPwn it should never be the only thing standing in front of tool invocation.
- Tenant isolation evaluated per tool call. Authentication ("which agent is this?") must feed authorization ("which tenant's resources may it touch?") on every invocation, not once at session setup. A session that outlives a permission change — or that was minted without ever checking permissions — replays the
/mcpvs/mcp_messagesplit at platform scale. - Destructive tools behind explicit grants. Deploy, destroy, secret-rotate, and scale-to-zero are the platform equivalents of
nginx_config_modifyplus auto-reload. They deserve step-up confirmation, narrow scopes, and human-in-the-loop policies by default — because agents will call whatever the tool list offers, and attackers will call it too.
The uncomfortable truth MCPwn exposes is that MCP servers are being added to admin surfaces faster than admin-surface discipline is being applied to them. The protocol makes exposing powerful tools easy; nothing in the framework makes securing them automatic. Until that changes, the checklist above is the manual compensating control — for a single-box admin UI and for a multi-tenant deploy API alike.
MCPwn will not be the last MCP CVE — it was only the first actively exploited one. Somewhere right now, another team is wiring an MCP endpoint "for speed," and the question is whether their route table gets audited before or after it shows up on Shodan. Audit yours this week: dump the routes, test anonymous access, flip the defaults to deny. It takes an afternoon, and it is considerably cheaper than a CVSS 9.8.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Agents are first-class operators there, which is exactly why its deploy API treats every agent-facing surface as untrusted until proven otherwise. Star the repo on GitHub or deploy your first app today.



