Filesystem MCP server security: what allowed directories do, and how they were escaped

The filesystem MCP server keeps an agent inside the directories you list. In 2025 two advisories showed that boundary leaking through a name prefix and through symlinks. Here's the setup we'd use now.

The short answer

The reference filesystem MCP server (@modelcontextprotocol/server-filesystem) restricts every operation to the allowed directories you pass when starting it.1 That boundary is the whole security model, so three things matter: run a version from after the July 2025 fixes (0.6.3 or 2025.7.01 and later, per the advisories), list the narrowest directories the task needs, and keep anything sensitive (.env, SSH keys, other projects) outside them.23 If the agent only needs to read, run the server in a container with the directory mounted read-only.1

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

How allowed directories work

You name directories when you start the server:

Code Data in JSON, the format software uses to pass records and settings.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/projects/app"]
    }
  }
}

The README calls command-line directories the method that works on every protocol version.1 There’s a second way: a client that supports MCP Roots can change the allowed directories at runtime, and when it does, the roots completely replace the server-side list. Roots are deprecated as of protocol version 2026-07-28.1

That replacement rule is worth a moment. If your client sends roots, the directories in your config are not the boundary any more; the client’s roots are. Check what your client sends before relying on the command line.

The two 2025 advisories

Both were reported by Elad Beber of Cymulate and fixed in the same releases.

Advisory Problem Severity (GitHub) Affected Patched
GHSA-hc55-p739-j48w, CVE-2025-53110 Path validation bypass via colliding path prefix: access to unintended files when the prefix matches an allowed directory High, CVSS v4 7.3 before 0.6.3 and 2025.3.28 0.6.3, 2025.7.012
GHSA-q66q-fx2p-7w4m, CVE-2025-53109 Path validation bypass via symlink handling: access to unintended files via symlinks within allowed directories High, CVSS v4 8.4 before 0.6.3 and 2025.3.28 0.6.3, 2025.7.013

Both advisories advise upgrading to 0.6.4 or 2025.7.01, while listing 0.6.3 as patched.2 The safe reading: use 2025.7.01 or later, or 0.6.4 or later on the older line. Other databases rate these issues differently, so we quote GitHub’s own scores.

Why a prefix check fails

The first advisory is a classic bug: checking that a path “starts with” the allowed directory. We wrote both checks in a few lines of Node to see the difference:

Code Program code, for developers. The text around it says what it does.

import path from 'node:path';
const allowed = '/projects/app';
const naive = (p) => path.resolve(p).startsWith(allowed);
const safer = (p) => {
  const rel = path.relative(allowed, path.resolve(p));
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
};

Output for four paths:

Code What the screen or file shows, copied as is.

/projects/app/src/index.js           naive: true  safer: true
/projects/app_secrets/.env           naive: true  safer: false
/projects/app/../app_secrets/.env    naive: true  safer: false
/etc/passwd                          naive: false safer: false

/projects/app_secrets begins with the characters /projects/app, so a string prefix check lets it through. The relative-path check asks the right question: is the target inside the directory, after resolving ..?

Symlinks are the second layer. Even a correct path check is fooled if a link inside the allowed directory points outside it, unless the server resolves the link’s real path before checking. That’s what the second advisory is about.3 Your own tools that touch files have the same two traps.

Which tools write

The README lists the read-only tools (read_text_file, read_media_file, read_multiple_files, list_directory, list_directory_with_sizes, directory_tree, search_files, get_file_info, list_allowed_directories) and four that change things: write_file, which creates or overwrites (“exercise caution with this”); edit_file; create_directory; and move_file.1

Two more notes from the README: a read-only file is refused rather than replaced, and overwrites are not crash-atomic, so a crash mid-write can leave a file partly written.1 An agent that rewrites a config file and gets interrupted can leave it broken.

If your client lets you disable individual tools, turn off the four writers for read-only tasks. If it doesn’t, use the container approach below.

A safer setup

Narrow directories. Name /projects/app/src, not your home directory. Every directory listed is reachable in full.

Keep secrets out. .env files, ~/.ssh, cloud credentials and other projects should sit outside every allowed directory. The filesystem server plus a web-fetch tool is all three legs of the lethal trifecta if the agent can read a secret and request a URL.

Read-only in a container. The README’s Docker setup mounts directories under /projects, and adding the ro flag makes a mount read-only to the server.1 That holds even if the server has a bug, because the operating system refuses the write:

Code Commands typed in a terminal. The text around it says what happens.

docker run -i --rm \
  --mount type=bind,src=/home/me/app/src,dst=/projects/src,ro \
  mcp/filesystem /projects

No symlinks pointing out. Before you list a directory, check it for links that lead elsewhere: find /path -type l -exec ls -l {} +.

Pin the version. npx -y @modelcontextprotocol/server-filesystem fetches whatever is newest; pin a known version in your config and update deliberately.

If you build your own file tool

Many agents don’t use the reference server at all; they ship a custom “read file” or “save file” tool. The same rules apply, and they’re easy to get wrong in a hurry:

  1. Resolve, then compare. Turn the requested path into an absolute path, resolve .., then check it’s inside the base directory with a relative-path test like safer above, never a string prefix.
  2. Resolve links before checking. Use the real path of the target (in Node, fs.realpath) and of the base directory, so a link can’t point the check one way and the write another.
  3. Refuse rather than repair. If a path fails the check, return an error that says so. Don’t strip .. and carry on; the model will learn nothing and may try another spelling.
  4. Write to a temporary file, then rename. That avoids the half-written file the README warns about, because a rename within one filesystem replaces the old file in one step.

Test each rule with the four paths in our table before an agent ever calls the tool.

A checklist

  • Server version is 2025.7.01 or later (or 0.6.4 or later).
  • Allowed directories are the narrowest that work.
  • You know whether your client sends Roots, which replace the configured list.
  • No secrets or other projects inside allowed directories.
  • No symlinks inside them pointing outside.
  • Read-only tasks run with a read-only mount or with the four write tools disabled.
  • The agent can’t also fetch arbitrary URLs in the same session, or a person approves each fetch.

For the wider picture of what a local server can do to you, see local MCP server security and MCP server security basics. To see which configured servers can write on your machine, paste the config into the check page or run npx deedglass scan (how the grade works); to try the checks in your browser first, open the playground.

Written with AI assistance and checked against our claims register.

The takeaway

A startsWith check let /projects/app_secrets pass for /projects/app in our test; the filesystem server fixed that and a symlink escape in 0.6.3 and 2025.7.01.

Read nextSecuring a local MCP server: the Origin check, the Host check and DNS rebinding Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1modelcontextprotocol/servers: Filesystem MCP server README · github.com · checked 26 October 2026
  2. 2GHSA-hc55-p739-j48w: Path validation bypass via colliding path prefix · github.com · checked 26 October 2026
  3. 3GHSA-q66q-fx2p-7w4m: Path validation bypass via symlink handling · github.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing