DEV Community

MCP Security Notes
MCP Security Notes

Posted on

6 ways MCP servers get pwned — and the code that stops each one

MCP (Model Context Protocol) servers hand an LLM real tools: read a file, run a command, fetch a URL. The problem: the arguments to those tools are driven by untrusted content — a web page, a document, another model. That is a textbook injection surface, and MCP servers are shipping with the same six holes over and over. Here is each one with a copy-paste fix.

1. Path traversal → arbitrary file read

A read_file tool that does open(base + user_path) leaks /etc/passwd the moment the model is told to read ../../../../etc/passwd. Symlinks and absolute paths bypass naive prefix checks.

from pathlib import Path

def resolve_within(base: str, user_path: str) -> Path | None:
    base_p = Path(base).resolve()
    target = (base_p / user_path).resolve()   # resolves .. and symlinks
    if base_p not in target.parents and target != base_p:
        return None                            # refuse, don't clamp
    return target
Enter fullscreen mode Exit fullscreen mode

Refuse on None. Never "clean" the path and continue — that is how the regression re-opens.

2. Command execution → RCE

subprocess.run(f"convert {name}", shell=True) is game over. Any tool arg the model influences is attacker-controlled.

subprocess.run(["convert", name], shell=False, timeout=10)  # argv list, never a string
Enter fullscreen mode Exit fullscreen mode

Whitelist the args the user actually controls; reject anything not on the list.

3. Unsafe deserialization → RCE

pickle.load, torch.load (pre-2.6 default), yaml.load without a safe loader, and numpy.load(allow_pickle=True) all execute code on load. This is huntr's most-paid MCP/AI bug class.

import yaml
yaml.safe_load(data)                     # not yaml.load
torch.load(f, weights_only=True)         # not the default on old versions
# for models: prefer safetensors over pickle entirely
Enter fullscreen mode Exit fullscreen mode

4. SSRF in URL-fetching tools

A fetch_url tool will happily hit http://169.254.169.254/ (cloud metadata) or http://localhost:6379. Resolve the host and block private ranges before connecting.

import ipaddress, socket
def is_public(host: str) -> bool:
    ip = ipaddress.ip_address(socket.gethostbyname(host))
    return not (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved)
Enter fullscreen mode Exit fullscreen mode

Also disable redirects and cap the response size.

5. Secret leakage

Don't put keys in tool code or echo request data back unsanitized. Bound every input's length and type; a 50MB "filename" is an attack, not a filename.

6. No pre-deploy check

Run this every release:

grep -rn "shell=True\|os.system\|subprocess.call(" src/
grep -rn "pickle.load\|yaml.load(\|torch.load(\|allow_pickle=True" src/
grep -rn "open(\|Path(\|send_file\|extractall" src/
Enter fullscreen mode Exit fullscreen mode

Then fire the attacker payloads (../, 169.254.169.254, a pickle with a __reduce__) at a local instance and confirm every one is refused.


I audit MCP servers and AI/ML packages for a living. If you want the full thing — a symlink/absolute-proof path guard with its call-site regression test, the SSRF resolver, the safe-deserialization swaps, and the exact attacker payloads to fire at your own server — I packaged it as a MCP Server Security Hardening Kit ($19). The pre-deploy checklist above is free; the kit is the code that makes every box pass.

Ship it secure.


Free tool: paste your MCP server's tool code into the MCP Server Security Scanner and get instant findings across all six vuln classes — path traversal, command injection, unsafe deserialization, SSRF, hardcoded secrets and input bounds. 100% client-side, your code never leaves the browser.

Top comments (0)