Answers · MCP security
How do I block SSRF and path traversal when an MCP tool takes a URL or file path argument, or a resource template like file:///{path}?
Last updated
TL;DR: Treat every URL and path the model hands your server as attacker input, because it can be: the value comes from a model that may have read injected text. The MCP spec (2026-07-28) says servers MUST validate all tool inputs and all resource URIs, and MUST sanitize file paths to prevent directory traversal when serving file:// resources. For URLs, don't fetch whatever arrives: take a host (or a choice from a list), match it against an allowlist, and build the request yourself with a fixed scheme, port and path, as OWASP's SSRF cheat sheet advises. Where you must accept arbitrary public URLs, resolve the name, reject private, loopback, link-local (169.254.169.254) and other reserved addresses with a vetted IP library rather than your own parser, connect to the address you checked, and re-check every redirect hop. For paths, resolve symlinks and compare the real path to the allowed directory on a path-separator boundary, never with startsWith: that exact mistake let the official Filesystem MCP server reach /private/tmp/allow_dir_sensitive_credentials when only /private/tmp/allow_dir was allowed (CVE-2025-53110), and a symlink fallback bug let it reach any file (CVE-2025-53109); both were fixed in 2025.7.1.
What should I check, step by step?
1. List every tool argument and resource template variable that becomes a URL, hostname, file path or filename. Those are the inputs to lock down; template variables such as {path} in file:///{path} are filled in by the client, not by you.
- Prefer an id over a location: let the tool take a report id, a repo name or an index (OWASP's path traversal advice is to use indexes rather than file names) and map it to the real URL or path on the server.
- Fix the allowed set in server configuration, not in the call. The 2026-07-28 spec deprecates Roots and points to tool parameters, resource URIs or server configuration instead; the allowed hosts and directories belong in config the model can't change.
- For outbound requests, allow https only, check the host against an allowlist with an exact match, and build the URL from your own scheme, port and path; don't copy the caller's path or query through.
- If the host can be any public site, resolve it, reject every address that isn't public (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, 0.0.0.0/8, fc00::/7, fe80::/10, ::1, multicast) using a vetted library, then connect to that same address so a second DNS lookup can't swap it (the DNS-rebinding TOCTOU the MCP spec warns about).
- Turn off automatic redirects and validate each hop with the same rules, or send all fetches through an egress proxy such as Smokescreen that blocks internal destinations by design.
- For files: decode the input once, reject NUL bytes and absolute paths, join it to the allowed directory, resolve the real path (and the real parent for files that don't exist yet), and accept only if the real path equals the allowed directory or sits under it followed by a separator. If any step errors, deny.
- Test with known-bad inputs before release: ../ and %2e%2e%2f, a sibling directory that shares the allowed prefix, a symlink pointing outside, http://169.254.169.254/, http://localhost:6379/, a redirect to an internal address, and http://example.com\@evil.com.
Why are MCP tool URL and file path arguments an SSRF and path traversal risk?
The model picks the arguments, and the model reads untrusted text: web pages, tickets, files. A sentence in a fetched page can steer a fetch_url tool to a cloud metadata address or a read_file tool to ~/.ssh. Your server is the last place that can say no, which is why the tools section of the spec puts input validation on servers as a MUST, alongside access controls and rate limits.
- The spec's own SSRF section is written for MCP clients fetching OAuth discovery URLs a malicious server supplies, but the attack list applies to any server-side fetch: direct internal IPs, the 169.254.169.254 metadata endpoint that can hand out cloud credentials, localhost services such as Redis, DNS rebinding and redirect chains. Its advice carries over too: HTTPS only, block private and reserved ranges, validate redirect targets, consider an egress proxy, and don't hand-roll IP parsing because octal, hex and IPv4-mapped IPv6 encodings slip past custom parsers.
- Resources add one more path in. resources/read takes a URI, and a template such as file:///{path} turns whatever fills {path} into a file location. The spec requires servers to validate resource URIs and sanitize file:// paths against directory traversal, and to return a -32602 error, not an empty result, for a resource that doesn't exist.
How should an MCP server validate a URL argument before fetching it?
When the tool only needs a few known services, OWASP's cheat sheet says not to accept full URLs at all: accept a host or a choice, compare it against an explicit allowlist, and build the request from the matching entry plus a scheme, port and path your code fixes. That removes the parser games entirely.
- OWASP also shows why parsing is a trap: for http://example.com\@evil.com a WHATWG-style parser reads the host as example.com, while CPython's urllib.parse returns evil.com. If the URL is parsed again anywhere downstream, reject one whose host different parsers disagree on.
- When the target can be any public site (a webhook or fetch tool), you are on a deny-list, which OWASP calls a last resort. Resolve all A and AAAA records, reject any non-public address, connect to the checked IP while keeping the original hostname for the Host header, TLS SNI and certificate checks, apply the same rule to retries and redirects, and add a network-level egress rule so a bug in the code isn't the only barrier. On AWS, require IMDSv2 as defense in depth.
How should an MCP server check a file path argument stays inside the allowed directory?
CVE-2025-53110 (GHSA-hc55-p739-j48w) is the lesson in one line: the official @modelcontextprotocol/server-filesystem checked normalizedRequested.startsWith(dir), so with /private/tmp/allow_dir allowed, Cymulate's researcher listed and read /private/tmp/allow_dir_sensitive_credentials. Compare on a boundary: allowed only if the resolved path equals the directory or starts with the directory plus the path separator, or use a relative-path check that rejects results starting with .. or absolute.
- CVE-2025-53109 (GHSA-q66q-fx2p-7w4m) showed the second trap. The server did resolve symlinks, but when the real-path check threw, its error handler fell back to checking the parent of the symlink's own path, which passed, so a symlink to /etc/sudoers was readable and writable; Cymulate went on to code execution by writing a macOS Launch Agent. Check the resolved target, and treat any error in the check as a denial.
- Both affected versions up to 0.6.2 and 2025.1.14 up to (not including) 2025.7.1; the fix is 2025.7.1. If you run that server, confirm the installed version. If you write your own, also run it in a container or OS sandbox where the allowed directory is the only one mounted, so a validation bug can't reach the rest of the disk.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Record the destinations my MCP fetch and file tools are allowed (the exact API hosts and the one folder) as a declared intent, so a reviewer can later compare any URL or path a tool touched against what was allowed.”
{
"tool": "declare_intent",
"arguments": {
"agent_id": "<one of your agents>",
"purpose": "other",
"description": "MCP server 'docs-fetch': fetch_url may reach only api.example.com and docs.example.com over https; read_file may read only /srv/mcp/docs (read-only). No redirects followed, no other hosts or paths.",
"operations": [
"record_ai_run"
],
"ttl_hours": 720,
"targets": {
"domains": [
"api.example.com",
"docs.example.com"
],
"resources": [
"mcp-server:docs-fetch",
"folder:/srv/mcp/docs:ro"
]
}
}
}- Needs your account API key. Rightshold returns a canonical intent statement for the agent's key to sign (sign_intent); a child intent declared beneath it must keep its domains and resources within the parent's.
- This is a record, not a filter. Rightshold doesn't see your server's fetches or file reads and can't block an SSRF or traversal; the URL and path checks above run in your server, and the egress and mount limits in your network and container.
- precall_check (free) takes the https URL of a remote MCP server you're about to connect and reports what's on record about it, including its current tool definitions, so you can see which tools take URLs or paths before you allow it.
Rightshold records declared intents and checks remote MCP servers you ask about. It does not validate URLs or paths inside your MCP server, proxy its outbound requests, or scan your code for SSRF or traversal bugs. Not legal advice.
What are the sources?
- MCP specification 2026-07-28: Tools (security considerations: servers MUST validate all tool inputs)
- MCP specification 2026-07-28: Resources (resource templates, file:// traversal, error codes)
- MCP specification 2026-07-28: Security Best Practices (Server-Side Request Forgery)
- MCP specification 2026-07-28: Deprecated Features (Roots, Sampling, Logging)
- OWASP: Server-Side Request Forgery Prevention Cheat Sheet
- OWASP: Path Traversal
- GitHub Advisory GHSA-hc55-p739-j48w (CVE-2025-53110): Filesystem MCP server path prefix bypass
- GitHub Advisory GHSA-q66q-fx2p-7w4m (CVE-2025-53109): Filesystem MCP server prefix and symlink bypass
- Cymulate: EscapeRoute, breaking the scope of Anthropic's Filesystem MCP server (CVE-2025-53109 and CVE-2025-53110)
Related
- How to protect a local MCP server from DNS rebinding
- How to sandbox a local stdio MCP server to only the files and hosts it needs
- How to stop prompt injection in fetched web or file content from triggering MCP write tools
- How to restrict network egress for AI coding agents
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
