Topic: mcp server ssrf

MCP Server SSRF — Server-Side Request Forgery in MCP Tool Handlers

Server-Side Request Forgery is the most prevalent security finding in the 101-server MCP corpus: present in 50% of F-grade repos and in 36.7% of the corpus overall. The mechanism is straightforward — a tool handler accepts a URL from the LLM's tool call arguments and passes it directly to fetch() — but the threat model is different from web-application SSRF because the attacker is the model itself, not an unauthenticated HTTP client.

How SSRF enters MCP tool handlers

In a traditional web application, SSRF occurs when an attacker-controlled URL reaches a server-side HTTP client. In an MCP server, the URL comes from the LLM's tool call arguments. This changes the threat model in two important ways:

The three SSRF patterns found most frequently in the corpus:

  1. Direct URL passthrough. const res = await fetch(args.url) — the URL is taken directly from tool arguments with no validation. Appears in 28% of corpus servers.
  2. Template URL construction. const res = await fetch(`https://api.example.com/${args.path}`) — the origin is fixed but the path or query string comes from args. Less severe than full URL passthrough but still exploitable via path traversal (args.path = "../../internal/admin").
  3. Redirect following. Handlers that use fetch(url) with redirect following enabled (the default in most HTTP clients) allow an SSRF-protected external URL to redirect to a private address. Always set { redirect: "error" } or { redirect: "manual" }.

Detection: what static analysis catches vs what it misses

The SkillAudit engine's static SSRF check uses a taint-flow analysis: it tracks whether any string derived from args.* in a tool handler body reaches a fetch(), axios.*, got(), node-fetch(), httpx.get(), or requests.get() call without an intervening validation step. This catches patterns 1 and 2 above reliably.

What static analysis misses: indirect flows where args influence a URL through intermediate variables or utility functions. The engine's inter-procedural analysis (tracing across function calls) handles one level of indirection but may miss chains of three or more. For those cases, the LLM-assisted probe in the Security axis submits crafted SSRF payloads through the handler and observes the response — this catches indirect SSRF that the taint graph would miss at the expense of requiring a live handler environment.

Prevention: three patterns that eliminate SSRF

// Pattern 1: origin allowlist (recommended for most servers)
const ALLOWED_ORIGINS = new Set([
  "https://api.yourservice.com",
  "https://cdn.yourservice.com"
]);
function safeUrl(rawUrl: string): URL {
  const u = new URL(rawUrl); // throws on malformed URL
  if (!ALLOWED_ORIGINS.has(u.origin)) throw new Error("URL not in allowlist");
  return u;
}

// Pattern 2: private-range denylist (for servers that need arbitrary external URLs)
const PRIVATE_RE = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|169\.254\.|::1|fc00:|fe80:)/;
async function safeFetch(rawUrl: string, opts?: RequestInit) {
  const u = new URL(rawUrl);
  if (PRIVATE_RE.test(u.hostname)) throw new Error("Private IP not allowed");
  return fetch(u.toString(), { ...opts, redirect: "error" }); // no open redirects
}

// Pattern 3: no user-controlled URL at all — fixed endpoint, args become query params
server.tool("search_docs", { query: z.string().max(200) }, async ({ query }) => {
  const url = new URL("https://api.yourservice.com/search");
  url.searchParams.set("q", query); // query is a value, not a URL segment
  const res = await fetch(url.toString());
  return res.json();
});

For servers where pattern 3 is viable (fixed endpoint, args are values not URL components), it eliminates SSRF entirely at the design level. Pattern 1 is the right default for most servers. Pattern 2 is a fallback for servers that genuinely need to fetch from arbitrary external URLs — use it with aggressive monitoring of outbound request logs.

Check your MCP server for SSRF vulnerabilities in 60 seconds.

Run a free audit → See corpus grades →