Topic: mcp server network security

MCP server network security — transport, TLS, and outbound request controls

MCP servers that make outbound network requests are operating at a position most web application firewalls don't cover: they're a child process on the developer's local machine, outside the corporate perimeter. When an LLM instructs a tool to fetch a URL, that request goes out through the developer's network, with access to localhost, internal services, and cloud metadata endpoints. 50% of the SkillAudit corpus has SSRF findings — the most common single finding class — almost all of which are network security failures: no origin allowlist, no redirect following limits, no private IP range blocking.

Transport mode: stdio vs HTTP-transport

Before discussing outbound network controls, it's worth distinguishing the two MCP transport modes, because they have different inbound network security postures:

stdio transport — the server communicates with the host client via stdin/stdout. It has no network listening port and is not reachable from the network. The network risk is entirely outbound: what the server calls, not what calls it. Most community MCP servers use stdio.

HTTP-transport — the server exposes an HTTP endpoint that the host client calls. This introduces inbound network risk: who can reach the endpoint, whether it requires authentication, and whether it runs over TLS. HTTP-transport servers are more common in team-deployment scenarios. SkillAudit checks for:

SSRF: the most common network finding

SSRF (Server-Side Request Forgery) in MCP servers differs from web application SSRF in one key way: the attack is initiated by the LLM following prompt-injection instructions, not by a human attacker sending a malicious HTTP request. The server is the victim and the unwitting attacker simultaneously.

The three SSRF patterns in the corpus, in order of frequency:

  1. Direct URL passthrough — a URL argument passed directly to fetch() with no validation
  2. Template URL construction — a user-supplied string interpolated into a hardcoded URL template (`https://api.example.com/data/${userInput}`)
  3. Redirect following to private addresses — a fetch to a public URL that redirects to a private range address (169.254.x.x, 10.x.x.x, 172.16-31.x.x, 192.168.x.x)

The fix for patterns 1 and 2 is an origin allowlist. The fix for pattern 3 is a redirect policy that checks the final destination before following:

import { isPrivateIP } from "./utils/ip.js"; // your own RFC1918 check

async function safeFetch(url, options = {}) {
  const parsed = new URL(url);

  // Block private ranges before first request
  if (isPrivateIP(parsed.hostname)) {
    throw new Error(`blocked: private range address ${parsed.hostname}`);
  }

  const res = await fetch(url, { ...options, redirect: "manual" });

  // Follow redirects manually, checking each destination
  if (res.status >= 300 && res.status < 400) {
    const dest = res.headers.get("location");
    if (!dest) throw new Error("redirect with no Location header");
    return safeFetch(dest, options); // recursive check
  }

  return res;
}

TLS enforcement for external API calls

MCP servers that call external APIs over plain HTTP rather than HTTPS expose credentials and response data to interception. This is a WARN-level finding on the Security axis (not a HIGH, because exploitation requires network position), but it's a straightforward fix that most authors make immediately when flagged.

Node.js has no equivalent of Python's PYTHONHTTPSVERIFY=0 foot-gun at the application level, but two patterns generate a WARN in the corpus:

Both patterns are grepped for in the Security axis scan. The fix is to remove them from production code paths (use environment flags to toggle for development only) and configure proper CA certificates for self-signed cert environments.

Private IP range blocking

Any MCP server that accepts URL arguments and makes outbound requests needs a private-range denylist. The ranges to block:

// RFC1918 private ranges + link-local + loopback
const PRIVATE_RANGES = [
  /^127\./,                   // loopback
  /^10\./,                    // RFC1918 Class A
  /^172\.(1[6-9]|2\d|3[01])\./, // RFC1918 Class B
  /^192\.168\./,              // RFC1918 Class C
  /^169\.254\./,              // link-local (AWS metadata: 169.254.169.254)
  /^::1$/,                    // IPv6 loopback
  /^fc00:/i,                  // IPv6 ULA
  /^fe80:/i,                  // IPv6 link-local
];

export function isPrivateIP(hostname) {
  return PRIVATE_RANGES.some(re => re.test(hostname));
}

The AWS metadata endpoint at 169.254.169.254 is the most targeted private address in SSRF attacks — fetching it from an EC2-hosted MCP server can return the instance's IAM credentials. The link-local range block covers it.

DNS rebinding

DNS rebinding is an advanced network attack where a domain that initially resolves to a public IP switches to a private IP between the allowlist check and the actual fetch. It's not checked by static analysis (it requires a DNS lookup at scan time). SkillAudit flags it as an INFO-level note when it detects that a server uses domain-name allowlists without also checking the resolved IP. The mitigation is to resolve the hostname to an IP, check that IP against the private-range denylist, and then pass the resolved IP to the fetch rather than re-resolving the domain.

For the full SSRF finding breakdown across the 101-server corpus, see the SSRF in MCP servers page. For the input validation patterns that prevent URL arguments from reaching network calls, see MCP server input validation.

Check your server's network security posture

SkillAudit's Security axis checks for all SSRF patterns, TLS disablement, and private-range access — with file/line locations and specific remediation code.

Run a free audit