0.0.0.0:8000 "to test from a phone." It has no auth, because "only my agent talks to it." A web page the developer visits runs fetch() against localhost:8000, lists the tools, and invokes the database-query tool. No password. No token. No prompt. That is the class behind the headlines about MCP "rug-pulls" and "tool poisoning": not a clever supply-chain trick, but a tool surface exposed over a network transport with no authentication boundary. The precedent is exactly one CVE old: CVE-2026-33032 (nginx-ui), an agent-adjacent control surface reachable over the network with weak-to-absent auth. This piece walks the decision tree from an exposed MCP server to unauthenticated tool invocation, and the auth boundary that closes it. It is the companion to our earlier piece on MCP tool poisoning, rug-pulls, and line-jumping.
The Model Context Protocol is the wire between an AI model and the tools it can call. It is a genuinely good abstraction, and it grew explosively across 2025 and 2026. That growth happened faster than the hardening norms, and a large fraction of MCP servers were written under one quiet assumption: localhost is trusted. That assumption is the same one that produced a decade of unauthenticated-localhost-service CVEs: exposed debug ports, dev servers without a token, Docker's API on 0.0.0.0, Jupyter with no password, Electron IPC bridges. MCP is the newest member of that family, and the model-invokes-tools architecture raises the stakes: the thing on the other side of the missing auth boundary is not a debug endpoint, it is a set of actions a language model was given permission to take.
This is part of our attack-research series. Where the companion piece walked tool poisoning, a malicious tool description that hijacks the model, this one walks the more basic failure: the server invokes a tool for a caller it never authenticated. By the end you should be able to fingerprint your own MCP exposure, walk the four-probe decision tree, and ship the auth-and-origin boundary that makes unauthenticated invocation structurally impossible. Verifiable security.
The attack pattern in one paragraph
An MCP server exposes two RPC surfaces over an HTTP or SSE transport: tools/list, which enumerates the tools the server offers, and tools/call, which invokes one. The server's author assumed only the local trusted client would ever reach these, so they did not wire an authentication check, and they did not validate the request's Origin. Two independent exposures follow. First, the direct network exposure: if the server binds a routable interface (or is placed behind a reverse proxy), any host that can reach the port can list and invoke tools with a plain HTTP request, no credentials. Second, the browser/DNS-rebinding exposure: even if the server binds loopback, a web page the victim visits can issue requests to http://localhost:<port>, and without an Origin check the server accepts them, or, via DNS rebinding, an attacker-controlled domain re-resolves to 127.0.0.1 and the same-origin policy no longer protects the loopback service. In both cases the attacker crosses exactly one boundary: unauthenticated (or cross-origin) caller → invocation of a tool the server intended only its trusted local client to invoke. That crossing is the bug. A read-only tool call is the whole proof; no chaining, no destructive call required to demonstrate it.
The unifying observation: MCP makes tool invocation a network-reachable RPC, but a large fraction of servers gate it as if it were a local function call. The attacker lives in that gap.
Why this still ships in 2026
The MCP spec and the official SDKs do describe authentication, specifically OAuth2 for the HTTP transport. Mature servers on the SDK's recommended happy path are largely fine. So why is the class alive? We did a static source review of the official Anthropic Python SDK (mcp 1.27.2, fetched once from PyPI and read offline) and the reasons are concrete and on the record.
- DNS-rebinding / Origin protection is off by default. In
mcp/server/transport_security.py, theTransportSecurityMiddleware, when constructed with no settings, defaults toenable_dns_rebinding_protection=False. The source comment states the reason verbatim: "If not specified, disable DNS rebinding protection by default for backwards compatibility." Host and Origin validation is therefore off unless the developer opts in. A server that wires the low-levelstreamable_httporssetransport directly without passingsecurity_settingsruns with no Origin or Host check at all. - The high-level path only auto-protects on loopback. The FastMCP path does auto-enable DNS-rebinding protection, but only when
hostis127.0.0.1,localhost, or::1. A FastMCP server deliberately bound to0.0.0.0"to test from another device" skips that auto-enable. The exact configuration a developer reaches for when they want network access is the one that drops the guard. - Auth is opt-in. FastMCP requires authentication only if the developer supplies an
AuthSettingsplus a token verifier or auth-server provider. Default construction = no token verifier = tool invocation accepted with no bearer token. The README example does not include auth. The blog tutorial does not include auth. The internal proof-of-concept that quietly became production does not include auth. - The Origin check is permissive by design. Even when protection is enabled, an absent
Originheader passes the check (correct for non-browser clients). That means the Origin gate stops browser/cross-origin callers but does nothing against a direct non-browser network client, which is exactly the direct-exposure path.
None of these is a bug in the SDK. They are documented, intentional configuration responsibilities, and the SDK's recommended path (FastMCP on localhost) ships secure-by-default. The residual risk lives in hand-rolled servers and low-level-transport servers that never opt in, which, in a young ecosystem that grew under "localhost is trusted," is a large and growing population. CVE-2026-33032 is the first CVE-numbered instance of the class; it will not be the last.
The attacker decision tree
The five-step tree from a reachable MCP transport to unauthenticated tool execution. Our probe stops at step 4.
The decisive step is 2: four cheap checks that each answer "is there an auth boundary here, and is it enforced on this surface?" The attacker does not need all four to fail: any single "yes that should have been no" is a foothold. A reachable transport plus an unauthenticated tools/call is a direct-network compromise; a missing Origin check is a browser/DNS-rebinding compromise that needs only that the victim visit a web page. Both are scriptable in seconds.
A composite real-world scenario
The setting is a mid-size engineering org that adopted an AI coding assistant. To give the assistant repository and database context, a platform engineer stood up an internal MCP server using the low-level streamable-HTTP transport, not FastMCP, because they wanted custom routing. The server exposes three tools: read_file, run_sql (read-replica, "safe"), and http_get (for fetching internal API docs). To let teammates point their assistants at it, they bound 0.0.0.0:8000 behind the office reverse proxy. They wired the transport directly and never passed security_settings, so there is no Origin check; and they never supplied an AuthSettings, so there is no token verifier. "It's internal," the design doc says.
An attacker who has any foothold that can reach the internal network, whether a compromised laptop, a guest-WiFi pivot, or a malicious dependency running in CI, finds the server with a one-line fingerprint and lists its tools with no credentials:
# Step 1-2: fingerprint + unauthenticated tools/list (JSON-RPC over HTTP)
$ curl -s http://mcp.internal:8000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
{"jsonrpc":"2.0","id":1,"result":{"tools":[
{"name":"read_file","inputSchema":{...}},
{"name":"run_sql","inputSchema":{...}},
{"name":"http_get","inputSchema":{...}}]}}
No bearer token was sent. The server answered. That is the boundary crossing: an unauthenticated caller obtained the tool surface. Our probe would stop one call later, after a single read-only invocation that confirms tools/call also requires no auth:
# Step 3-4: one read-only call is the whole proof. Stop here.
$ curl -s http://mcp.internal:8000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"read_file","arguments":{"path":"README.md"}}}'
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"# Internal Platform ..."}]}}
A real attacker does not stop. run_sql "read-replica safe" still reads every row of every customer table the replica carries. http_get is a server-side request forgery primitive pointed at the cloud metadata endpoint, the exact SSRF-to-credential-theft chain we walked in an earlier piece, now reachable with zero authentication through a tool the AI agent was handed. And the browser variant needs no internal foothold at all: a developer running this server on their own laptop, bound even to loopback, can be hit by any web page they visit, because the missing Origin check plus DNS rebinding defeats the same-origin protection. The page lists the tools and calls run_sql from the victim's own machine.
The reason it works is not exotic. The server treated a network-reachable RPC as if it were a trusted local function call. No authentication, no origin validation, the transport's own backwards-compatibility default left the guard off, and the developer never opted in.
What we observe in customer environments
We are honest about scope and about the limits of testing. We only ever lab-test our own builds of open-source MCP servers, in disposable containers with no egress after fetch, and any probe we ship against a customer's own flagged assets is non-destructive: a tool list, a single read-only call, an Origin check, never a destructive invocation, never a chain. Within those limits, surveying open-source MCP servers and customer-flagged internal deployments over the past months, the rough shape:
- The official-SDK happy path holds. FastMCP-on-localhost servers we reviewed shipped Origin/Host protection on by default, as the source shows. The class is refuted at the SDK level: we drafted no CVE against
mcp1.27.2. - Hand-rolled and low-level-transport servers are where it lives. A meaningful fraction of servers that wired
streamable_httporssedirectly never passedsecurity_settings, running with no Origin check, and never supplied a token verifier. - Binding
0.0.0.0without auth was common wherever a developer wanted "access from another device," silently dropping the FastMCP loopback auto-protection. - The missing-
Origin-check / browser-reachable variant was the most under-appreciated: developers who correctly bound loopback still had no defence against a malicious page plus DNS rebinding.
The honest read, matching our research disposition: there is no novel spec-level or official-SDK CVE here, and we do not claim one. The risk is a per-project class against hand-rolled servers, plus a configuration-discipline gap, and it is real because the default-off behaviours above are real and documented in the source.
What to do about it: the MCP auth boundary contract
The fix is to give the tool surface an authentication boundary and an origin boundary, and to never assume the transport gave you either for free.
MCP server hardening contract: controls that end the class
- Bind loopback unless you have a deliberate, authenticated reason not to.
host="127.0.0.1"is the safe default. Binding0.0.0.0is a decision that requires auth and Origin validation, not a convenience. - Always pass
security_settingsto low-level transports. If you wirestreamable_httporssedirectly, constructTransportSecuritySettings(enable_dns_rebinding_protection=True, allowed_hosts=[...], allowed_origins=[...])explicitly. Do not rely on the backwards-compatible default: it is off. - Require authentication on the tool surface. Supply an
AuthSettingswith a real token verifier (the OAuth2 flow the spec describes). No token verifier means tool invocation is accepted with no bearer token. Treat an unauthenticatedtools/callas a P1. - Validate
OriginandHostagainst an allow-list for any browser-reachable deployment. Remember the absent-Origin pass-through: it is correct for non-browser clients but means Origin validation alone does not stop a direct network client. You need auth and Origin, not either alone. - Scope the tools the model can reach to least privilege. A
run_sqltool should be read-only against a minimal view; anhttp_gettool must deny RFC-1918 and metadata targets (no SSRF pivot). Assume the auth boundary will someday fail and minimise the blast radius behind it. - Inventory every MCP server in your org and re-check on a schedule. The proof-of-concept that became production is the dangerous one. Treat MCP transports as first-class attack surface, fingerprinted and re-probed like any other listening service.
MCP makes tool invocation a network RPC. A large fraction of servers gate it like a local function call. The fix is an auth boundary and an origin boundary, and never assuming the transport gave you either by default.
The audit, concretely, is a four-probe sweep against every MCP transport you can find:
# 1) reachable from a non-loopback origin? (should be: no, unless authed)
$ nc -z -w2 mcp.internal 8000 && echo "REACHABLE off-host"
# 2) tools/list without a token? (should be: 401 / rejected)
$ curl -s -o /dev/null -w '%{http_code}\n' http://mcp.internal:8000/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# 3) tools/call without a token? (should be: 401 / rejected)
# (one read-only tool only; stop on first success — that IS the proof)
# 4) Origin validated? send a hostile Origin; should be rejected
$ curl -s -o /dev/null -w '%{http_code}\n' http://mcp.internal:8000/mcp \
-H 'Origin: https://evil.example' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Any surface that returns the tool list or executes a call without a token, or accepts a hostile Origin, has no boundary. The sweep is finishable in an afternoon for any org that knows where its MCP servers run, and the first task is usually finding out that they run in more places than anyone documented.
How Celvex catches this
Find. Prove. Fix. Verify.
The scanner fingerprints MCP transports (JSON-RPC tools/list banner, SSE endpoints) and runs the four-probe auth-posture family, MCP-AUTH-001..004, covering non-loopback reachability, unauthenticated list and call, and Origin validation.
For a confirmed exposure we ship a Proof Capsule with the exact unauthenticated request, a single read-only tool response, and a replay script, against the customer's own lab build. One read-only call is the proof; we never chain or invoke a destructive tool.
The Capsule's remediation block points at the boundary that failed: pass security_settings, supply a token verifier, bind loopback, or scope the tool, with the exact SDK option and the line to change.
After the fix lands, the unauthenticated call returns 401 and the hostile Origin is rejected. The finding closes automatically and the dashboard records the verified-fix event for the audit trail.
Where we sit on the autonomy curve: at L1.5 today, the MCP-AUTH-001..004 posture probes are grounded in the real default-off behaviour we read in the SDK source, not hypothesis, and ship as a non-destructive test family extending our existing MCP-discovery tests. At L2 within 90 days, the corpus adds the DNS-rebinding browser-reachability variant and tool-schema risk scoring (flagging shell/write/SSRF-capable tools behind a weak boundary). At L3 within twelve months, the scanner synthesises transport-specific probes for unfamiliar hand-rolled MCP servers it fingerprints in customer environments, with a strict refuse-non-RFC1918-target guard. We do not claim L3 today, and we do not claim a novel SDK-level CVE: the SDK happy path is secure-by-default. We claim our L1.5 catches the unauthenticated-invocation and missing-Origin exposures reliably and ships a reproducible Capsule for each.
Bottom line
The MCP "rug-pull" headlines describe a symptom. The class underneath is older than MCP: a tool surface exposed over a network transport with no authentication boundary, on the assumption that only a trusted local client will ever call it. CVE-2026-33032 is the first CVE-numbered instance; the official SDK ships DNS-rebinding protection off by default and auth opt-in, so the happy path is safe but every hand-rolled or low-level-transport server that never opts in is exposed. The model-invokes-tools architecture is what raises the stakes: the thing behind the missing boundary is a set of actions a language model can take, including shell, file write, and SSRF. The fix is a contract: bind loopback, pass security_settings, require a token verifier, validate Origin and auth, and scope every tool to least privilege. Until that boundary exists, every exposed MCP server is one unauthenticated tools/call away from handing an attacker the agent's own hands.
Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.
Sources
- NVD: CVE-2026-33032 (nginx-ui agent-adjacent unauthenticated control surface)
- Model Context Protocol: Authorization specification (OAuth2 for HTTP transport)
- Anthropic: MCP Python SDK (
mcp) source, incl. transport_security.py - PyPI:
mcppackage (v1.27.2 reviewed) - OWASP: Server-Side Request Forgery (the
http_gettool pivot) - DNS rebinding: defeating same-origin protection on loopback services
- CELVEX Group: MCP tool poisoning, rug-pulls, and line-jumping (companion)
- CELVEX Group: Proof Capsule format
Probe your own MCP servers.
Free Exposure Check, no signup required. We fingerprint your MCP transports and run the four-probe auth-posture family, then ship a Proof Capsule for the highest-confidence unauthenticated-invocation exposure.
Run a Free Scan →