cd /news/ai-tools/a-path-traversal-guard-for-mcp-file-… · home topics ai-tools article
[ARTICLE · art-91844] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

A path-traversal guard for MCP file tools that actually survives symlinks

A developer released a path-traversal guard for MCP file tools that withstands symlink-based escapes, addressing a critical security flaw in servers exposing read_file, write_file, or list_dir tools. The guard uses canonical path resolution and strict containment checks, refusing to clamp or repair malicious inputs. The developer also provides regression tests and a free security scanner for MCP servers.

read2 min views1 publishedAug 11, 2026

If your MCP server exposes a read_file

/ write_file

/ list_dir

tool, it is one clever prompt away from serving /etc/passwd

to whoever controls the model's input. The naive fixes — prefix checks, os.path.normpath

, stripping ..

— all fail against symlinks and absolute paths. Here is a guard that holds, plus the regression test that keeps it holding.

if not user_path.startswith(BASE):   # "/base/../etc/passwd".startswith("/base") is True
    reject()

open(os.path.join(BASE, os.path.normpath(user_path)))  # normpath doesn't resolve symlinks

normpath

is pure string math. A symlink inside BASE

that points to /

turns a "safe" relative path into a full-filesystem read. Absolute paths (/etc/passwd

) sail straight through a join in many languages.

from pathlib import Path

def resolve_within(base: str, user_path: str) -> Path | None:
    base_p = Path(base).resolve(strict=True)      # canonical, symlinks followed
    target = (base_p / user_path).resolve(strict=False)
    if target == base_p or base_p in target.parents:
        return target
    return None                                   # REFUSE — never clamp/repair

Two rules that matter more than the code:

user_path.replace("..","")

) is where the regression re-opens six months later. Return None

and error out..resolve()

on the target, not just the base...

A guard without a regression test rots. Fire the actual attacker payloads at it:

import pytest
from mymcp.paths import resolve_within

BASE = "/srv/sandbox"

@pytest.mark.parametrize("evil", [
    "../../../../etc/passwd",
    "/etc/passwd",
    "..%2f..%2fetc%2fpasswd",     # if you url-decode before calling, test the decoded form too
    "sub/../../etc/passwd",
    "./././../etc/shadow",
])
def test_traversal_refused(evil):
    assert resolve_within(BASE, evil) is None

def test_symlink_escape_refused(tmp_path):
    base = tmp_path / "sandbox"; base.mkdir()
    (base / "link").symlink_to("/etc")            # symlink out of the sandbox
    assert resolve_within(str(base), "link/passwd") is None

def test_legit_path_allowed(tmp_path):
    base = tmp_path / "sandbox"; base.mkdir()
    (base / "notes.txt").write_text("ok")
    assert resolve_within(str(base), "notes.txt") is not None

If test_symlink_escape_refused

passes, you have beaten the class of bug that string-based guards miss.

The regression that bites is a second file tool added later that opens paths directly and forgets to route through resolve_within

. Grep every release:

grep -rnE "open\(|Path\(|send_file|shutil\.(copy|move)" src/ | grep -v resolve_within

Every hit is a call site to audit.

This is one guard of six I keep in a hardening kit for MCP servers — path containment, argv-only subprocess, safe deserialization, an SSRF resolver for fetch_url

tools, input bounds, and a pre-deploy grep+payload checklist with tests. If you want the whole set as copy-paste code, it's the ** MCP Server Security Hardening Kit ($19)**. The guard and tests above are yours free — ship them today.

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.

── more in #ai-tools 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-path-traversal-gua…] indexed:0 read:2min 2026-08-11 ·