From broken authorization to skipped confirmations, our MCP research uncovered one recurring problem: the control isn’t where the action happens.
We audited 30+ MCP servers, gateways, clients and frameworks with roughly 200 million downloads in the past 30 days. One pattern dominates: an MCP server advertises a restriction it does not actually enforce. One finding is already CVE-2026-79746 (CVSS 8.1). Here is the gap, pattern by pattern, with the code that breaks, the weakness class, and the fix.
An MCP server is usually a thin layer of glue. A model asks it to do something, and the server turns that request into something real on the other side: a database query, a shell command, an API call that spends your credentials, or a file written to disk.
We spent months auditing the public MCP server ecosystem from the outside in. We discovered and filed security issues across highly popular 30+ widely used MCP servers, gateways, clients and packages. Several of them were rated high severity. To put things in perspective, these projects saw roughly 200 million downloads in the last 30 days, the top one being an MCP server framework which has been downloaded ~99 million times. A flaw in the framework has much higher impact than an application, it impacts everything built on top of it.
Almost every serious bug comes down to the same gap: the server claims a restriction, but the restriction does not sit on the path the request actually takes. Our first finding from this study is already a published CVE, CVE-2026-79746 (CVSS 8.1 - https://github.com/samanhappy/mcphub/security/advisories/GHSA-454m-4vm6-842f) and the study has since produced a run of coordinated disclosures ranging up to one-click remote code execution in widely used clients. The patterns below run in that order, from the quietest config mistake to a hostile server that runs code on the user's own machine. Each comes with the mechanism, the weakness class, and the fix.
Every issue behind this post went to the vendor first, through HackerOne, Bugcrowd, or a GitHub Security Advisory, and nothing which is still under private coordination is named here.
This vulnerability was discovered in mcphub, which is a centralized gateway to manage and scale multiple MCP servers by organizing them into HTTP endpoints, thereby supporting access to all servers, individual servers, or logical server groups.
The mcphub gateway is the clearest public example of missing enforcement of claimed restrictions vulnerability (GHSA-454m-4vm6-842f, CVE-2026-79746, CVSS 8.1; affected < 1.0.31, fixed in 1.0.31).
It lets you mint an access key scoped to specific backend servers, then group servers behind one address. The connect-time authorization check used loose overlap logic instead of containment.

.some() returns true if a single server in the group is on the key's list, so a key scoped to one harmless public server unlocks every server grouped beside it. Bundle a public server and a sensitive internal one into a group, hand out a key for only the public one, and that key reaches the internal server the moment it is pointed at the group. A routine setup choice becomes privilege escalation.
The same shape appears wherever authorization and action live in different places: a guard on one transport but not the WebSocket beside it, or a check performed once at connect that nothing repeats per call, etc.
The check runs, but with the wrong predicate and at the wrong time (connect, not call).
Decide permission at the moment of the action, not just at the door. Require full containment (.every()), not overlap, and make sure every route and every transport passes through the same authorization middleware.
The first gap is about who gets through the door. This one is about what they can do once inside. Many servers offer a read-only mode and enforce it with a text match, and a text check loses to the database's own parser every time.
We reproduced this against a popular MCP server, where /* c */DROP TABLE customers slipped past the keyword check (since patched upstream; the maintainers now describe that guard as best-effort, not a security boundary).
The root cause is a parser mismatch: the database tokenizer treats a block comment as whitespace, so the statement the engine executes and the string the check inspected diverge.
The class recurs across database MCP servers in different shapes, a keyword list that blocks DELETE but not GRANT, a per-tool isWrite:false flag hung on a method that writes, a classifier that files any statement it cannot recognize (an UPSERT, a data-modifying WITH ... DELETE, a stored-procedure call) as "read" and lets it through.
A syntactic string match standing in for a semantic guarantee.
Do not decide safety by matching text. Enforce read-only where it is real: connect with a database account that has no write permission, so the guarantee holds for every query the engine will accept, comment tricks included.
Those first two gaps needed a crafted request to open them. This one can trip on its own. Dangerous tools often claim a human is in the loop, then quietly fail open the moment the confirmation channel is unavailable.

We found this across several infrastructure and identity MCP servers: a destructive operation ran with no confirmation because the gate defaulted to "approved" whenever it could not ask. The variants are always a control that fails open, an auto-confirm fallback when the client cannot prompt, a caller-settable confirm flag that the party being controlled gets to supply, or a normalization gap that lets a spelling variant of the operation slip the check entirely.
The safe state on error is refusal, and the code chose the opposite.
A confirmation must fail closed. If there is no way to ask a human, refuse the action. Never let the party being controlled supply the "already confirmed" signal.
Those three gaps all live inside the server's own logic. This one breaks a boundary the deployer thought they had drawn. Deployers use allow-lists to limit which tools an agent may call, and too often the filter is applied only when building the list shown to the model, not at execution. The menu is filtered; the kitchen is not.

We found this in several payment and infrastructure toolkits. The model is shown list_orders and get_status; the executor still holds refund_payment and delete_account and will run either one if the name arrives in a tool call. If a hallucinating or prompt-injected model emits a name that was never on the menu, the executor prepares the order anyway. The desync is the whole bug: the security decision was made against one set of tools and the dispatch happens against another.
Enforcement at list-build time, dispatch against the unfiltered catalog.
The executor is the security boundary, not the menu builder. Re-check the allow-list at dispatch, against exactly the same filtered set the model was given.
So far the tool has done more than it advertised. Here it does exactly what it is told, using a value it should never have trusted. Security-critical values, file paths and outbound URLs above all, must come from server-side configuration, never from the untrusted model or a connected server. When they do not, two classic sinks open up.

We found server-side request forgery where a server-supplied fragment of the request URL let the target host be rewritten to an internal or cloud-metadata address, with the response handed straight back to the model.
The mechanism is a URL-parsing quirk: when an attacker-controlled path segment begins with @host, a naive base + route concatenation makes the trusted base into URL "userinfo" and the parser resolves the attacker's host instead.
A close cousin lives in schema handling: a tool's input schema, fully controlled by a connected server, is passed to a JSON $ref resolver whose default loader will fetch http:// and read file:// references, turning schema processing at agent startup into blind SSRF and local file read.
We also found path-traversal writes where a model-supplied name climbed out of the intended directory and dropped attacker-chosen bytes elsewhere on disk.
Trust placed in a value that crossed the wire.
Treat every value from the model or a connected server as hostile. Contain every path (resolve to an absolute path, then verify it is still inside the root). Allow-list outbound hosts, reject userinfo and host changes, and pin DNS between the check
Most MCP bugs are not exotic. They are a check that lives one layer away from where it is needed, and a server's advertised restriction is only as real as the thing that enforces it on the path the action actually takes.
Audit your execution boundaries today. Put every check where the action happens, not just at intake. Enforce read-only with a real database account, make every gate fail closed, re-check the allow-list in the executor, contain paths, allow-list outbound hosts, and treat both the client you talk to and the servers your agent connects to as hostile.
Give each tool the minimum credentials it truly needs, assume a server's advertised restrictions are best-effort until you have verified them yourself, and never point an agent at a registry server you would not be willing to run yourself.
The discussions in this post point at one thing: MCP servers are just like any other software, they may have vulnerabilities, they may fail to enforce the safety which they claim. That is the gap Metano is built to close. We are building runtime security for the AI Agents ecosystem. Rather than trusting each MCP server's advertised limits, Metano gives security teams control over which MCP servers and tools to allow / deny in their network. Along with that, Metano also provides realtime visibility and threat detection during the invocation of MCP server tools as and when they get invoked by AI Agents.