GUIDE · MCP 9 min ·

SSRF is the MCP bug that steals your cloud keysHow a tool call reaches your metadata endpoint.

An MCP server with a fetch tool is an SSRF primitive waiting for a prompt. If a tool takes a URL and the server requests it without an allowlist, an attacker can point that request at your cloud metadata endpoint and walk away with the host's IAM credentials.

TL;DR· the answer, in twenty seconds

What: Server-side request forgery is the second most common way an MCP server gets popped. A tool takes a URL, the server fetches it with no destination allowlist, and an attacker points it at http://169.254.169.254/ to read the host's cloud IAM credentials. CloudSEK reports SSRF reaches 36.7% of analyzed MCP deployments.

Fix: Allowlist the destinations a fetch tool may reach. Block internal IP ranges and the file://, gopher://, and dict:// schemes. Require IMDSv2 on every EC2 instance so a bare GET cannot read the metadata endpoint. Do not leave standing cloud credentials in the server's environment.

Lesson: A fetch tool is an outbound request primitive that an LLM will aim wherever the context tells it to. The destination is the security boundary, and most servers do not check it.

The second most common way an MCP server gets popped in 2026 is server-side request forgery. A tool accepts a URL, the server fetches it, and nothing checks where the request goes. Point that request at the cloud metadata endpoint and it returns the host's IAM credentials. This is CWE-918, the same SSRF that has been draining cloud accounts since the Capital One breach, now wired to a model that will type the malicious URL for the attacker.

CloudSEK puts the base rate at 36.7% of analyzed MCP server deployments reachable by SSRF. That tracks with the Endor Labs scan of 2,614 implementations, where 82% touch file operations and a large share proxy or fetch remote content. The command-injection problem is the bug everyone names first. SSRF is the one sitting right behind it, and it is quieter because it does not need a shell.

The bug: a tool that fetches a URL you do not control

An MCP server exposes tools. Some of those tools fetch things: a "summarize this article" tool, a "download the audio at this link" tool, a "check this webhook" tool, a thumbnail generator that pulls an image. Each one takes a URL and makes an outbound request. The URL arrives in a JSON-RPC call the model assembled, and the model assembled it from context: the user's prompt, a file it read, a web page it summarized, the output of another tool. None of that is trusted.

The vulnerable shape is short:

# vulnerable: the server fetches whatever URL the tool call carries
@mcp.tool()
def fetch_audio(url: str) -> bytes:
    resp = httpx.get(url, follow_redirects=True)
    return resp.content

httpx.get(url) will request anything. A public CDN link, sure. Also the metadata path at 169.254.169.254/latest/meta-data/iam/security-credentials/, the AWS Instance Metadata Service. Also localhost:6379 to poke Redis. Also file:///proc/self/environ to read the process environment off disk. The function does not distinguish between "an audio file on the internet" and "a secret on the loopback interface" because it never looks at the destination. It looks at the response, and it hands that response back through the protocol.

The model does not need to be malicious. It needs to be convinced once that the URL it should pass is the attacker's. A poisoned issue body, a crafted error message, a README the agent summarized: any of those can carry the metadata URL into the argument. The agent calls the tool with it, the server fetches it, and the JSON-RPC response carries the credentials back to whoever is steering the conversation.

The CloudSEK case: one audio tool, two protocols, full account

This is not hypothetical. CloudSEK documented an unauthenticated MCP server running inside a Spring Boot application that handled voice and messaging. The server exposed an audio download utility that took a URL and fetched it. No authentication on the endpoint, no allowlist on the URL.

The first move was SSRF to the metadata service. An attacker called the tool with 169.254.169.254 as the host in the URL. The server made the request from inside the VPC, where the metadata endpoint answers, and returned a JSON payload that proxied straight back through the server. Walk the standard IMDS paths and the response includes the EC2 instance's IAM role name and then its live AccessKeyId, SecretAccessKey, and Token. Those are real, usable credentials with whatever the instance role grants, which on a typical app server is more than anyone audited.

The same tool accepted a second protocol. Swap the scheme to file:// and the audio proxy became a local file reader. The attacker requested file:///proc/self/environ and got back the process environment as plaintext, which held the database credentials the app had loaded as environment variables. One unvalidated URL parameter yielded both the cloud identity and the database password. SSRF for the role, LFI for the env.

Two failures stack here, and they are the same failure twice. The tool trusted the destination of an outbound request, and it trusted the scheme of that request. Fix either and the chain breaks. Fix the destination and the metadata endpoint is unreachable. Fix the scheme and file:// stops reading your disk. A server that did both would have fetched the audio file and refused everything else.

Why the metadata endpoint is the prize

The cloud metadata service exists so an instance can learn about itself and fetch the credentials for its attached role without anyone baking a key into an AMI. On AWS it lives at the link-local address 169.254.169.254, which is reachable only from the instance itself. That last property is what makes SSRF valuable: the attacker cannot reach 169.254.169.254 from the internet, but your server can, and an SSRF turns your server into the attacker's proxy onto that address. The EC2 metadata SSRF technique is one of the best-documented moves in cloud exploitation, and the payoff is short-lived but real role credentials.

The defense AWS shipped is IMDSv2. Version 1 answers a plain GET to the metadata path, which is what makes it trivial to reach through an SSRF that can only issue a GET. IMDSv2 requires a session: the caller first sends a PUT to get a token, then includes that token as a header on the GET. Most SSRF primitives can only do the GET, so requiring IMDSv2 neutralizes the bare metadata read on its own:

# IMDSv1: one GET, no token, SSRF-reachable
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

# IMDSv2: a PUT for a token first, then the GET carries it
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

Requiring IMDSv2 is one of the few controls that costs you nothing and closes a whole attack class. Set the instance metadata options to http-tokens=required and the GET-only SSRF stops returning credentials. GCP and Azure have their own metadata endpoints with their own header requirements; the principle holds across all three.

CVE-2026-39974 and the base rate

The pattern keeps shipping as named CVEs. CVE-2026-39974 is an SSRF in n8n-MCP, the MCP server that exposes the n8n automation platform to agents. Rated CVSS 8.5, fixed in 2.47.4, disclosed in April 2026. The flaw let an attacker with valid credentials make the server issue HTTP requests to arbitrary URLs supplied through HTTP headers, with the response data returned over JSON-RPC. The advisory names the obvious target: cloud metadata services on AWS, GCP, and Azure, plus internal hosts the server can reach and nothing outside can. The fix in 2.47.4 added URL validation that rejects embedded credentials and requests to restricted destinations. That is the allowlist, arriving late.

The reason this is a category and not a one-off is the count of servers built to fetch. Knostic verified 119 internet-exposed MCP servers by hand and found every one of them granted unauthenticated access to its tool listing, so an attacker can enumerate which tools fetch URLs before sending a single payload. Pair an open tool listing with a fetch tool and no destination check, and the SSRF is a two-request operation: list the tools, call the one that fetches. The MCP server security checklist covers the install-time questions for servers you run; this is the build-time one for servers you write.

How to not ship this

The defenses are old web-security controls, applied to a new caller.

Allowlist the destination. A fetch tool does not need to reach the entire internet. If the tool downloads audio from one provider, the allowlist is that provider's domains. If it summarizes user-supplied links, you still resolve the hostname, check the resolved IP against a denylist of internal ranges, and block the request before it leaves. Resolve first, then validate the resolved address, so a hostname that points at 169.254.169.254 or 127.0.0.1 cannot slip past a name-based check:

import ipaddress, socket
from urllib.parse import urlparse

ALLOWED_SCHEMES = {"http", "https"}

def safe_fetch(url: str) -> bytes:
    parsed = urlparse(url)
    if parsed.scheme not in ALLOWED_SCHEMES:
        raise ValueError(f"scheme not allowed: {parsed.scheme}")
    # resolve and reject internal / link-local / loopback targets
    for info in socket.getaddrinfo(parsed.hostname, None):
        ip = ipaddress.ip_address(info[4][0])
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            raise ValueError(f"blocked internal address: {ip}")
    return httpx.get(url, follow_redirects=False).content

Block the dangerous schemes. A fetch tool that proxies web content has no business honoring file://, gopher://, or dict://. Allowlist http and https and reject the rest. The CloudSEK chain needed file:// for the second half; an explicit scheme allowlist removes it.

Do not follow redirects without re-checking. A 302 pointing at 169.254.169.254 defeats a check that validated the original URL alone. Disable redirects on the fetch, or re-run the same address validation on every hop.

Require IMDSv2. Set http-tokens=required on every EC2 instance so the GET-only SSRF cannot read the metadata path. This is the backstop for the fetch tools you forgot about.

Do not leave standing credentials in the environment. The LFI half of the CloudSEK case worked because the database password sat in /proc/self/environ as a plaintext environment variable. An MCP server that loads long-lived secrets into its process environment turns any file read or any environment dump into a credential leak. Runtime secret injection and short-lived grants shrink what a successful read returns.

The NSA framed it as a trust-boundary problem

In May 2026 the NSA's Artificial Intelligence Security Center published security guidance for MCP deployments. The document treats tool inputs and tool outputs as places where untrusted data crosses a trust boundary, and it lists input validation plus least-privilege scoping as required mitigations rather than optional hardening. SSRF is the trust-boundary crossing made concrete: the URL is untrusted input, the outbound request crosses from your server's network position into places the attacker could never reach, and the response crosses back. Validate the input at the boundary, scope what the server can reach, and the crossing stops being a free proxy.

What this means for your stack

If you build MCP servers, grep your tool definitions for every place a URL becomes an outbound request this week. httpx.get, requests.get, fetch, urllib.request, any image or audio downloader, any webhook checker. For each one, add a scheme allowlist, resolve-then-validate the destination against internal ranges, and turn off automatic redirects. If you only run servers others wrote, assume better than a third of them will fetch a URL you hand them with no questions asked, and never point one at an instance that holds credentials worth stealing.

The architectural fix for the category is to stop letting a single fetch tool double as a key to your cloud account. The damage in the CloudSEK case came from two ambient grants: an instance role the SSRF could read and a database password the LFI could read. Neither was load-bearing for fetching audio. A server that holds short-lived, least-privilege references requested at the moment it acts, instead of a standing role and a plaintext env, turns the same SSRF into a request that returns nothing useful.

hasp is one working implementation of that idea. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and a server receives scoped, time-limited references in place of your raw keys and ambient environment. An SSRF still fires, but the metadata endpoint holds no role worth stealing and the environment holds no password to leak. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

None of that excuses the missing allowlist. Scoping the blast radius and validating the URL are different jobs, and a fetch tool owes its users both. Allowlist the destination, block file://, require IMDSv2. Then make sure that when one of your dependencies forgets to, the credential it borrows is not the one that owns your account.

Sources· cited above, in one place

NEXT STEP~90 seconds

Stop handing the agent your real keys.

hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.

  • Local, encrypted vault — no account, no cloud, no telemetry by default.
  • Brokered run — agent gets a reference, the child process gets the value.
  • Pre-commit + pre-push hooks catch managed values before they ship.
  • Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index