Securing Filesystem MCP Servers: Hardening Guide
Complete guide to securing MCP filesystem servers -- path traversal prevention, directory allowlisting, sandboxing, and audit logging.
A secure MCP filesystem server must enforce directory allowlisting, prevent path traversal attacks, block access to sensitive files, limit file sizes, and log every operation. Filesystem servers are among the most powerful -- and most dangerous -- MCP servers you can run. They give AI agents the ability to read, write, and manipulate files on your system, which means a misconfigured server can expose credentials, overwrite critical data, or serve as a foothold for deeper compromise.
This guide covers the security hardening steps every filesystem MCP server deployment needs, whether you are running the reference Filesystem MCP Server or building your own.
For the broader security context, see the MCP Security Model pillar guide.
Why Filesystem Servers Need Special Attention
Filesystem MCP servers are different from API-wrapping servers in a critical way: they operate directly on the host operating system's file system. A database MCP server is constrained by database permissions and network access. A filesystem server, by default, has access to anything the process owner can read or write.
Consider what an unrestricted filesystem server exposes:
| Risk | Example |
|---|---|
| Credential theft | Reading ~/.ssh/id_rsa, .env files, AWS credentials |
| Configuration tampering | Modifying .bashrc, cron jobs, system configs |
| Data exfiltration | Reading proprietary source code, documents, databases |
| Privilege escalation | Writing to paths that execute code (cron, shell profiles) |
| Denial of service | Filling disk with large file writes, deleting critical files |
The attack surface is the entire filesystem visible to the server process. Hardening is not optional.
Path Traversal Prevention
Path traversal is the most common attack against filesystem servers. An AI agent -- or a malicious prompt injected into the agent's context -- attempts to access files outside the intended directory using relative path components.
The Attack
If the server's allowed directory is /home/user/projects, a path traversal attack tries inputs like:
../../../etc/passwd
/home/user/projects/../../.ssh/id_rsa
/home/user/projects/subdir/../../../../etc/shadow
The Defense
Every path received from a tool call must be resolved to its canonical absolute form and then checked against the allowlist. Here is the correct approach:
import os
ALLOWED_DIRECTORIES = [
os.path.realpath("/home/user/projects"),
os.path.realpath("/home/user/documents"),
]
def validate_path(requested_path):
"""
Resolve the path and verify it falls within an allowed directory.
Returns the resolved path or raises an error.
"""
# Resolve to absolute, canonical path (resolves symlinks too)
resolved = os.path.realpath(requested_path)
# Check if the resolved path starts with any allowed directory
for allowed in ALLOWED_DIRECTORIES:
if resolved == allowed or resolved.startswith(allowed + os.sep):
return resolved
raise PermissionError(
f"Access denied: path is outside allowed directories"
)
Critical implementation details:
- Always use
realpath()(or equivalent), not justabspath(). Therealpathfunction resolves symbolic links, whichabspathdoes not. An attacker could create a symlink inside the allowed directory that points to/etc/. - Append the path separator before the
startswithcheck. Without it, an allowed path of/home/user/projectswould also match/home/user/projects-secret. - Validate on every operation. Do not validate once and cache the result, because the path could be different for each tool call.
Directory Allowlisting
The principle of least privilege means the filesystem server should only access directories the user explicitly permits. Here is how to configure and enforce allowlists.
Configuration Approach
Define allowed directories in the server's configuration file, not in code:
{
"allowedDirectories": [
"/home/user/projects/web-app",
"/home/user/projects/api-server",
"/tmp/mcp-workspace"
],
"readOnlyDirectories": [
"/home/user/reference-docs"
],
"blockedPatterns": [
"**/.env",
"**/.env.*",
"**/node_modules",
"**/.git/config",
"**/credentials*",
"**/*.pem",
"**/*.key",
"**/id_rsa*"
]
}
Granularity Levels
Different deployments need different levels of access:
| Level | Configuration | Use Case |
|---|---|---|
| Single directory | One allowed path | Working on one project |
| Project set | Multiple specific paths | Multi-repo development |
| Subtree | A parent directory and all children | Broad project access |
| Read-only subset | Some paths read-only, others writable | Reference material plus work dirs |
| Temporary workspace | /tmp subdirectory with auto-cleanup | Sandboxed experimentation |
Implementation Pattern
class DirectoryAllowlist:
def __init__(self, config):
self.writable = [
os.path.realpath(d) for d in config["allowedDirectories"]
]
self.readonly = [
os.path.realpath(d) for d in config.get("readOnlyDirectories", [])
]
self.blocked_patterns = config.get("blockedPatterns", [])
def check_access(self, path, operation="read"):
resolved = os.path.realpath(path)
# Check blocked patterns first
for pattern in self.blocked_patterns:
if self._matches_glob(resolved, pattern):
raise PermissionError("Access to this file pattern is blocked")
# For write operations, only writable directories are valid
if operation in ("write", "delete", "move"):
return self._is_within(resolved, self.writable)
# For read operations, both writable and readonly are valid
return self._is_within(resolved, self.writable + self.readonly)
def _is_within(self, path, directories):
for directory in directories:
if path == directory or path.startswith(directory + os.sep):
return True
raise PermissionError("Path is outside allowed directories")
def _matches_glob(self, path, pattern):
from fnmatch import fnmatch
return fnmatch(os.path.basename(path), pattern.replace("**/", ""))
Read-Only Mode
For many use cases, AI agents only need to read files -- not modify them. Running the filesystem server in read-only mode dramatically reduces risk.
When to Use Read-Only Mode
- Code review and analysis tasks
- Documentation lookup
- Log file inspection
- Configuration auditing
- Any task where the agent should observe but not change
Enforcement
Read-only mode should be enforced at the server level, not relied upon as a client-side configuration:
class ReadOnlyFilesystemServer:
"""
MCP filesystem server that only exposes read operations.
Write tools are not registered at all.
"""
def list_tools(self):
return [
# Only read operations are available
Tool(name="read_file", description="Read file contents"),
Tool(name="list_directory", description="List directory contents"),
Tool(name="search_files", description="Search for files by pattern"),
Tool(name="get_file_info", description="Get file metadata"),
]
# Note: write_file, create_directory, move_file, delete_file
# are intentionally NOT registered
By not registering write tools at all, the AI agent cannot even attempt write operations. This is stronger than registering write tools and rejecting them -- it removes the attack surface entirely.
File Size Limits
Unbounded file reads and writes create denial-of-service risks and can overwhelm the AI agent's context window.
| Operation | Recommended Limit | Reason |
|---|---|---|
| Read file | 10 MB | Prevents context window overflow |
| Write file | 50 MB | Prevents disk filling attacks |
| Directory listing | 1,000 entries | Prevents response explosion |
| Search results | 100 matches | Keeps results manageable |
| File upload | 100 MB | Protects disk space |
Implement limits at the tool handler level:
MAX_READ_SIZE = 10 * 1024 * 1024 # 10 MB
async def handle_read_file(path):
validated_path = validate_path(path)
file_size = os.path.getsize(validated_path)
if file_size > MAX_READ_SIZE:
return ToolResult(
content=f"File is too large ({file_size} bytes). "
f"Maximum allowed: {MAX_READ_SIZE} bytes. "
f"Use a more specific tool or read a portion of the file.",
is_error=True
)
with open(validated_path, "r") as f:
return ToolResult(content=f.read())
Blocking Sensitive Files
Even within allowed directories, certain files should never be accessible. A project directory might contain .env files with API keys, .git/config with repository credentials, or private key files.
Default Block List
Every filesystem MCP server should block these patterns by default:
# Environment and secrets
.env
.env.local
.env.production
.env.*.local
# Credentials and keys
*.pem
*.key
*.p12
*.pfx
id_rsa
id_ed25519
credentials.json
service-account.json
*.keystore
# Configuration with secrets
.git/config
.npmrc
.pypirc
.docker/config.json
.aws/credentials
.kube/config
# Sensitive system files
/etc/passwd
/etc/shadow
/etc/hosts
Implementation
import fnmatch
SENSITIVE_PATTERNS = [
".env", ".env.*", "*.pem", "*.key", "*.p12",
"id_rsa*", "id_ed25519*", "credentials*",
"service-account*.json", ".npmrc", ".pypirc",
".aws/credentials", ".kube/config",
]
def is_sensitive_file(filepath):
"""Check if a file matches any sensitive pattern."""
basename = os.path.basename(filepath)
relpath = filepath # Use full path for directory-based patterns
for pattern in SENSITIVE_PATTERNS:
if fnmatch.fnmatch(basename, pattern):
return True
if fnmatch.fnmatch(relpath, "*/" + pattern):
return True
return False
Importantly, block these files for all operations -- including listing. If a directory listing reveals that .env exists, that itself is information leakage, even if the agent cannot read the file's contents.
Sandboxing Techniques
For maximum isolation, run the filesystem MCP server in a sandboxed environment that physically limits what it can access.
Container-Based Sandboxing
Run the MCP server inside a Docker container with only the necessary directories mounted:
# docker-compose.yml for sandboxed filesystem MCP server
services:
mcp-filesystem:
image: mcp-filesystem-server:latest
volumes:
# Mount only specific directories, read-only where possible
- /home/user/projects/web-app:/workspace/web-app
- /home/user/docs:/workspace/docs:ro
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:size=100M
mem_limit: 512m
cpus: 1.0
network_mode: none
Key settings:
- Explicit volume mounts: Only the needed directories are visible inside the container
- Read-only root filesystem: The server cannot modify its own binaries
- No new privileges: Prevents privilege escalation
- Network disabled: The server cannot exfiltrate data over the network
- Resource limits: Prevents CPU and memory abuse
macOS Sandbox Profiles
On macOS, you can use the built-in sandbox facility:
(version 1)
(deny default)
(allow file-read* (subpath "/Users/dev/projects/web-app"))
(allow file-write* (subpath "/Users/dev/projects/web-app"))
(allow file-read* (subpath "/Users/dev/reference") )
(deny network*)
(deny process-exec)
Linux seccomp Profiles
On Linux, seccomp profiles can restrict the system calls available to the server process, preventing it from doing anything beyond basic file I/O.
Audit Logging
Every file operation performed through the MCP server should be logged. Audit logs are essential for detecting misuse, investigating incidents, and maintaining compliance.
What to Log
| Field | Example |
|---|---|
| Timestamp | 2026-02-26T14:30:00Z |
| Operation | read_file |
| Requested path | ../../../etc/passwd |
| Resolved path | /etc/passwd |
| Allowed | false |
| User/session | session_abc123 |
| Tool call ID | call_xyz789 |
| Result | PermissionError: path outside allowed directories |
Implementation
import logging
import json
from datetime import datetime, timezone
# Configure audit logger separate from application logger
audit_logger = logging.getLogger("mcp.audit")
audit_handler = logging.FileHandler("/var/log/mcp/filesystem-audit.jsonl")
audit_handler.setFormatter(logging.Formatter("%(message)s"))
audit_logger.addHandler(audit_handler)
audit_logger.setLevel(logging.INFO)
def audit_log(operation, requested_path, resolved_path, allowed, session_id,
error=None):
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"operation": operation,
"requested_path": requested_path,
"resolved_path": resolved_path,
"allowed": allowed,
"session_id": session_id,
}
if error:
entry["error"] = str(error)
audit_logger.info(json.dumps(entry))
Log Monitoring
Set up alerts for suspicious patterns:
- High volume of denied operations: May indicate a prompt injection attack probing for file access
- Access attempts to sensitive file patterns: Someone or something is trying to read credentials
- Unusual access times: File operations outside normal working hours
- Sequential directory traversal: Systematic exploration of the filesystem, which suggests automated reconnaissance
Security Hardening Checklist
Use this checklist when deploying any filesystem MCP server:
| Category | Check | Priority |
|---|---|---|
| Path validation | All paths resolved with realpath before access | Critical |
| Path validation | Symlinks resolved and validated | Critical |
| Allowlisting | Directories explicitly allowlisted in config | Critical |
| Allowlisting | Default-deny for all paths not in allowlist | Critical |
| Sensitive files | .env, keys, credentials blocked by default | Critical |
| Sensitive files | Block list applied to listings, not just reads | High |
| File limits | Read size limit enforced | High |
| File limits | Write size limit enforced | High |
| File limits | Directory listing pagination | Medium |
| Permissions | Read-only mode available and tested | High |
| Permissions | Write operations require explicit opt-in | High |
| Sandboxing | Container or OS-level isolation in production | High |
| Sandboxing | Network access disabled if not needed | High |
| Logging | All operations logged with full detail | High |
| Logging | Denied operations logged and alerted | Medium |
| Logging | Log files stored outside accessible directories | Medium |
| Testing | Path traversal test suite passes | Critical |
| Testing | Sensitive file access test suite passes | Critical |
What to Read Next
- MCP Security Model: Authentication, Permissions and Best Practices -- the parent guide covering the complete MCP security architecture
- MCP OAuth 2.1 Implementation Guide -- adding authentication to remote filesystem servers
- MCP Production Troubleshooting -- diagnosing errors when filesystem operations fail
- Filesystem and Document MCP Servers -- overview of available filesystem servers in the ecosystem
- Browse MCP Servers -- find filesystem servers in the directory