AI agents rely on MCP (Model Context Protocol) servers to extend their capabilities through tools, but path handling vulnerabilities remain one of the most common and dangerous attack vectors. When an agent processes file paths from untrusted input—whether from user prompts, external APIs, or tool outputs—insufficient validation creates openings for directory traversal attacks that can expose sensitive system files, escalate privileges, or compromise the entire agent infrastructure. Understanding exactly how your MCP server validates and resolves paths is not optional security hygiene; it is fundamental to safe agent operation.
The Path Validation Problem
Most MCP servers that interact with the filesystem implement tools like read_file, write_file, or list_directory. These tools accept path parameters that originate from LLM-generated content, which means they are inherently untrusted. The challenge is that path strings are deceptively complex: a path like ../../../etc/passwd might look innocent to pattern matching but resolves to a critical system file. Similarly, paths containing null bytes, Unicode normalization differences, or symbolic links can bypass naive validation logic.
The core issue is that many developers assume standard library path functions provide security guarantees they do not. Python's os.path.join(), Node's path.resolve(), and Go's filepath.Join() all perform path normalization, but they do not enforce security boundaries. A path constructed through these functions can still escape an intended directory if the input contains traversal sequences. Without explicit validation that checks the resolved path against an allowed root, your MCP server is vulnerable.
Implementing Robust Path Validation
Effective path validation requires a defense-in-depth approach. The first layer is input normalization using your language's canonical path resolution, followed by a strict boundary check against an allowed root directory. The resolved path must be verified to be within the intended scope before any filesystem operation occurs.
Consider this Python implementation for an MCP filesystem tool:
import os
from pathlib import Path
from typing import Optional
class SecurePathValidator:
def __init__(self, allowed_root: str):
# Resolve the allowed root to its canonical form
self.allowed_root = Path(allowed_root).resolve()
def validate(self, requested_path: str) -> Optional[Path]:
# Resolve the requested path relative to allowed root, then get canonical form
full_path = self.allowed_root / requested_path
resolved = full_path.resolve()
# Critical: Verify resolved path is within allowed root
try:
resolved.relative_to(self.allowed_root)
return resolved
except ValueError:
# Path escapes allowed root - reject
return None
def read_file_tool(path: str, validator: SecurePathValidator) -> str:
safe_path = validator.validate(path)
if safe_path is None:
raise PermissionError("Path escapes allowed directory")
with open(safe_path, 'r') as f:
return f.read()
This pattern ensures that even if the LLM generates a path like ../../../etc/shadow, the relative_to() check will fail because the resolved path falls outside the allowed root. The resolve() call handles symbolic links, eliminating symlink-based traversal attacks.
Additional Attack Vectors
Beyond basic directory traversal, MCP servers face several less obvious path-related risks. Unicode normalization attacks exploit the fact that different Unicode representations of the same character can bypass string-based filters while resolving to the same filesystem path. A path containing U+2024 (one dot leader) instead of U+002E (period) might slip past naive . filtering.
Null byte injection, though increasingly rare on modern systems, can still cause unexpected behavior in some contexts. Path components starting with - can be interpreted as command-line flags when passed to shell commands. And case sensitivity differences between Windows and Unix systems create portability risks—AllowedPath and allowedpath are different on Linux but the same on Windows, which can break security assumptions during cross-platform deployment.
Symbolic links require special attention. While resolve() follows symlinks and reveals their targets, some use cases legitimately need to access symlinked directories. In these cases, validate the symlink's target path against your allowed root before following it, rather than validating only the symlink location.
Audit Checklist for MCP Server Operators
Before deploying any MCP server with filesystem access, verify your path handling implementation:
- Canonical resolution: Does your code call
realpath,GetFullPathName, or equivalent before validation? - Root boundary enforcement: Is there an explicit check that resolved paths remain within an allowed directory?
- Input source identification: Can you trace every path parameter back to its origin (user prompt, tool output, external API)?
- Symlink handling: Are symlinks validated at their target, not just their location?
- Error exposure: Do validation failures return generic errors, or do they leak filesystem structure details?
- Platform testing: Have you tested path validation on both case-sensitive and case-insensitive filesystems?
If you cannot answer "yes" to questions 1-3 with confidence, your MCP server has a path traversal vulnerability. The attack surface for AI agents is broader than traditional applications because LLM-generated inputs are unpredictable and adversarial prompts are specifically designed to exploit edge cases in input handling. Path validation is not a feature you add later—it is foundational security architecture that must be correct from the first commit.