---
id: "GHSA-7m4x-rv66-mxhh"
page: "https://infrastack.ai/live/GHSA-7m4x-rv66-mxhh"
kind: "vulnerability"
aliases: ["GHSA-7m4x-rv66-mxhh"]
title: "server-filesystem: `move_file`'s same-device rename leaves the source unpinned \u2014 a swapped source parent moves an outside file into the allowed directories, where it becomes readable"
severity: "medium"
cvss: 5.0
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "@modelcontextprotocol/server-filesystem", "fixed": ["2.3.0"], "affected": "< 2.3.0", "ecosystem": "npm"}, {"name": "@modelcontextprotocol/server-filesystem", "fixed": [], "affected": ">= 2025.1.14, <= 2026.8.31", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["claude-desktop", "cwe-367", "filesystem", "mcp", "node", "package", "self-hosted"]
not_affected_if: ["auth-required", "no-untrusted-local-writers"]
published_at: "2026-11-11T07:24:37+00:00"
first_seen_at: "2026-11-11T07:28:35+00:00"
updated_at: "2026-11-11T07:29:58+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-7m4x-rv66-mxhh", "first_seen_at": "2026-11-11T07:28:35+00:00"}]
exploit_sources: []
---

# server-filesystem: `move_file`'s same-device rename leaves the source unpinned — a swapped source parent moves an outside file into the allowed directories, where it becomes readable

Link for people: https://infrastack.ai/live/GHSA-7m4x-rv66-mxhh (this .md is for agents)

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.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:@modelcontextprotocol/server-filesystem affected < 2.3.0 fixed 2.3.0
- npm:@modelcontextprotocol/server-filesystem affected >= 2025.1.14, <= 2026.8.31 fixed no fixed version listed

## How to check

- Check the installed version of `@modelcontextprotocol/server-filesystem` in your lockfile or with `npm ls @modelcontextprotocol/server-filesystem`.
- Confirm whether the server is run with directories a local, non-trusted process can write to.

## Fix

- `@modelcontextprotocol/server-filesystem`: move to `2.3.0` or later.

## Timeline

- 2026-11-11T07:28:35+00:00 ghsa-repo https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-7m4x-rv66-mxhh

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-7m4x-rv66-mxhh

## From the sources

### Summary

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.

### Details

Affected product / versions:
- Product: @modelcontextprotocol/server-filesystem (modelcontextprotocol/servers, `src/filesystem`)
- Confirmed version: v2.2.0, monorepo commit `36380287ed3b15d115b55f546617d723072b1296`
- Other versions: not verified. No claim is made about other versions.

Severity:
- Reporter's estimate (based on local reproduction against the real code, not a CNA assignment): CVSS v3.1 5.3 (Medium)
- Vector: `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N`
- CWE (proposed): CWE-367 (Time-of-check Time-of-use (TOCTOU) Race Condition) — the source path is validated and then handed to `fs.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.)
- Justification: as with the read-path race in the same component, exploitation requires a cooperating local process with write access inside an allowed directory, winning the window between validation and the rename — hence local vector and high complexity. A successful swap moves an arbitrary outside file into the allowed set (confidentiality High once it is read back; integrity Low because the file is removed from its original location).

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)),
  );
}
```
- On the doc comment's "outside this fix's scope": we read that as a scoping note about the earlier pinning change, not as a security acceptance of this gap — the same operation's other two paths (cross-device move, copy) pin the source, which indicates the source side is within the component's own security model, and the comment predates any analysis of the rename resolving a swapped source. We raise it so the maintainers can judge; if the scope note was intended as acceptance, the inconsistency with the sibling paths is still worth recording.
- Mechanism: (1) both arguments validate; the source's realpath is inside the allowed set at that moment; (2) `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:
- Reporter confidence in the underlying records is Low-to-Medium: the mechanism is code-certain and harness-reproduced, but exploitation needs the local-racer precondition and a won race window.
- The Windows export variant depends on a separate statically-supported observation about Windows write-path pinning that has not been dynamically verified on a Windows host; this report claims only the POSIX import form.
- No testing was performed against any third-party or production deployment.

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

### Proof of concept

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.

### Impact

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.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
