@MODELCONTEXTPROTOCOL/SERVER-FILESYSTEM · GHSA-7m4x-rv66-mxhh · Package
FIX → 2.3.0
The move_file handler checks the source and destination paths but, for same-device moves, passes the source path to a rename call that resolves it later, so a local process can swap a parent directory for a symlink and move an outside file into the allowed directories. Once inside, any read tool can return its contents.
first public Nov 11, 2026 · 1 source
Could this affect your app?
Free first check from your website. Connect your repo for the full answer.
Teams running the filesystem MCP server on a machine where another local process can write inside an allowed directory. The swap requires a cooperating local process and winning a race window, so single-user setups with no other local writers are much less exposed. Deployments where the allowed directories are read-only to other local users can skip it.
Not affected if auth-required, no-untrusted-local-writers
Who cares claude-desktop, filesystem, mcp, node, self-hosted
@modelcontextprotocol/server-filesystem in your lockfile or with npm ls @modelcontextprotocol/server-filesystem.Upgrade to 2.3.0
The move_file handler validates both its source and destination. For a same-device move, however, only the destination parent is pinned: renameIntoDirectory passes the source to fs.renameSync as an absolute string, which the kernel resolves at rename time — after validation. A local process that swaps a parent component of the source path for an outside-pointing symlink in that window causes the rename to move the outside file into the pinned destination inside the allowed directories, after which every read tool returns its contents: an out-of-bounds read achieved through move.
This is also an internal inconsistency in the component: the same operation's cross-device fallback (moveFileAcrossDevices) and copy_file both open the source through openSourceInDirectory — anchored at the source's own parent, lstat-checked, opened with O_NOFOLLOW, and identity-verified. Only the same-device rename leaves the source unpinned.
Affected product / versions:
src/filesystem)Check it with your coding agent
It reads this page and tells you if your code is affected.
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
@modelcontextprotocol/server-filesystem: move to 2.3.0 or later.36380287ed3b15d115b55f546617d723072b1296Severity:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:Nfs.renameSync as a string, which the kernel resolves at rename time; a component swapped in the window redirects the move. (The resulting escape from the restricted directory is of the CWE-22 class; the defect itself is the check-to-use race on the source, as in the read-path issue.)Details:
src/filesystem/pinned-directory.ts:325-336 (renameIntoDirectory), called from lib.ts:690-700 (moveFile); the handler validates the source at server.ts:909 and the destination at server.ts:913:/**
* Rename `sourcePath` (absolute, outside this fix's scope) to the final
* component of `destinationPath`, relative to its anchored directory.
*/
export function renameIntoDirectory(
sourcePath: string,
destinationPath: string,
): void {
inDirectory(path.dirname(destinationPath), () =>
fs.renameSync(sourcePath, path.basename(destinationPath)),
);
}
moveFile requires the destination to be absent and calls renameIntoDirectory; (3) the destination parent is anchored via inDirectory, but the source string is resolved by the kernel at rename time; (4) a swapped source parent redirects the resolution outside; (5) the outside file lands inside the allowed set and becomes readable by all read tools. On POSIX the destination pin means the file always lands inside (import, not export). On Windows the destination anchor itself is the subject of a separate, statically-supported observation (write-path pinning is weakened there); any export form of this issue depends on that observation and is not claimed here.Honest limitations:
Remediation direction:
Pin the source for the rename path as the sibling paths already do: open/anchor the source through openSourceInDirectory-equivalent machinery (anchored at the source's parent, identity-checked), or perform the rename relative to pinned directory handles for both ends (e.g. renameat-style with verified descriptors where the platform allows).
Reporter credit: Tomoki Murota, CISSP
Reproduction was performed locally against the real server code with the same harness family used for the read-path race (mc1_filesystem/verify/): the source's parent component was swapped for an outside-pointing symlink in the window between the handler's validation and the rename, and the outside file was moved into the allowed directory and subsequently read through the server (evidence: mc1_filesystem/FINDINGS_A.md Candidate 4; the finding survived an independent adversarial review, which examined and rejected the "out of scope by design" reading on the grounds above).
Illustrative trigger: move_file {source: "/allowed/sub/important.conf", destination: "/allowed/stolen.conf"} with /allowed/sub swapped for a symlink to /etc in the window: /etc/important.conf is moved into the allowed set and becomes readable.
A file outside the allowed directories can be moved inside them and read in full (POSIX form). The operation also deletes the file from its original location. The client's own tools cannot set up the swap; the cooperating-local-process precondition is the same one the component's write-path pinning already assumes.