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
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
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:
- 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 likesaferabove, never a string prefix. - 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. - 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. - 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.