Architecture & Components
Guide

Securing Filesystem MCP Servers: Hardening Guide

Complete guide to securing MCP filesystem servers -- path traversal prevention, directory allowlisting, sandboxing, and audit logging.

9 min read
Updated February 26, 2026
By MCPServerSpot Team

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:

RiskExample
Credential theftReading ~/.ssh/id_rsa, .env files, AWS credentials
Configuration tamperingModifying .bashrc, cron jobs, system configs
Data exfiltrationReading proprietary source code, documents, databases
Privilege escalationWriting to paths that execute code (cron, shell profiles)
Denial of serviceFilling 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 just abspath(). The realpath function resolves symbolic links, which abspath does not. An attacker could create a symlink inside the allowed directory that points to /etc/.
  • Append the path separator before the startswith check. Without it, an allowed path of /home/user/projects would 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:

LevelConfigurationUse Case
Single directoryOne allowed pathWorking on one project
Project setMultiple specific pathsMulti-repo development
SubtreeA parent directory and all childrenBroad project access
Read-only subsetSome paths read-only, others writableReference material plus work dirs
Temporary workspace/tmp subdirectory with auto-cleanupSandboxed 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.

OperationRecommended LimitReason
Read file10 MBPrevents context window overflow
Write file50 MBPrevents disk filling attacks
Directory listing1,000 entriesPrevents response explosion
Search results100 matchesKeeps results manageable
File upload100 MBProtects 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

FieldExample
Timestamp2026-02-26T14:30:00Z
Operationread_file
Requested path../../../etc/passwd
Resolved path/etc/passwd
Allowedfalse
User/sessionsession_abc123
Tool call IDcall_xyz789
ResultPermissionError: 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:

CategoryCheckPriority
Path validationAll paths resolved with realpath before accessCritical
Path validationSymlinks resolved and validatedCritical
AllowlistingDirectories explicitly allowlisted in configCritical
AllowlistingDefault-deny for all paths not in allowlistCritical
Sensitive files.env, keys, credentials blocked by defaultCritical
Sensitive filesBlock list applied to listings, not just readsHigh
File limitsRead size limit enforcedHigh
File limitsWrite size limit enforcedHigh
File limitsDirectory listing paginationMedium
PermissionsRead-only mode available and testedHigh
PermissionsWrite operations require explicit opt-inHigh
SandboxingContainer or OS-level isolation in productionHigh
SandboxingNetwork access disabled if not neededHigh
LoggingAll operations logged with full detailHigh
LoggingDenied operations logged and alertedMedium
LoggingLog files stored outside accessible directoriesMedium
TestingPath traversal test suite passesCritical
TestingSensitive file access test suite passesCritical

What to Read Next