---
id: "GHSA-7rgq-gccq-qphx"
page: "https://infrastack.ai/live/GHSA-7rgq-gccq-qphx"
kind: "vulnerability"
aliases: ["GHSA-7rgq-gccq-qphx"]
title: "Path traversal via trailing quote/whitespace in validatePath (Check/Use Mismatch)"
severity: "medium"
cvss: 4.2
epss: null
exploited: false
status: "open"
category: "ai-tooling"
products: [{"name": "@modelcontextprotocol/server-filesystem", "fixed": [], "affected": ">= 2025.8.21, <= 2026.8.31", "ecosystem": "npm"}, {"name": "@modelcontextprotocol/server-filesystem", "fixed": ["2.0.1"], "affected": ">= 1.0.0, < 2.0.1", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["ai-agents", "ai-tooling", "cwe-22", "cwe-436", "mcp", "node", "npm"]
not_affected_if: ["fixed-version-installed", "not-using-filesystem-mcp-server"]
published_at: "2026-10-25T00:18:38+00:00"
first_seen_at: "2026-11-09T22:53:25+00:00"
updated_at: "2026-11-09T22:57:17+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-7rgq-gccq-qphx", "first_seen_at": "2026-11-09T22:53:25+00:00"}]
exploit_sources: []
---

# Path traversal via trailing quote/whitespace in validatePath (Check/Use Mismatch)

Link for people: https://infrastack.ai/live/GHSA-7rgq-gccq-qphx (this .md is for agents)

The MCP filesystem server's path validation checks a normalized path but operates on the raw path, which strips a trailing quote or whitespace character. A request for a sibling directory such as /allowed/proj' passes the allowlist check, so an attacker who influences tool arguments can read, write, move or stat files outside the configured allowed directories.

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

## Affected

- npm:@modelcontextprotocol/server-filesystem affected >= 2025.8.21, <= 2026.8.31 fixed no fixed version listed
- npm:@modelcontextprotocol/server-filesystem affected >= 1.0.0, < 2.0.1 fixed 2.0.1

## How to check

- Check the installed version: `npm ls @modelcontextprotocol/server-filesystem`
- Vulnerable ranges per sources: `>= 1.0.0, < 2.0.1` and `>= 2025.8.21, <= 2026.8.31`
- Look for any MCP config that exposes the filesystem server to an LLM agent that reads untrusted content

## Fix

- `@modelcontextprotocol/server-filesystem` `2.0.1` (fixed version per the advisory's product data)
- Date-versioned installs (2025.x, 2026.x) will not be offered 2.0.1 by package managers; install it explicitly

## Timeline

- 2026-11-09T22:53:25+00:00 ghsa-repo https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-7rgq-gccq-qphx

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-7rgq-gccq-qphx

## From the sources

### Summary

validatePath in src/filesystem/lib.ts performs the allowlist containment check on normalizePath(absolute) but runs fs.realpath and returns the result from the raw absolute string. normalizePath strips a trailing single quote, double quote, or whitespace character. Therefore a request for /allowed/proj' passes the containment check (it normalizes to /allowed/proj) while the file operation runs on /allowed/proj' - a sibling of the allowed directory. All 14 filesystem tools route through validatePath and are affected.

### Details

## Root cause

The affected function is validatePath in src/filesystem/lib.ts.

The bug is a check/use mismatch between two strings derived from the same input:

1. normalizePath strips a leading or trailing quote/whitespace character:
     p = p.trim().replace(/^["']|["']$/g, "");

2. The allowlist check runs on normalizePath(absolute):
     const normalizedRequested = normalizePath(absolute);
     const isAllowed = isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories);

3. But the file operation and the returned value use the raw absolute string:
     const realPath = await fs.realpath(absolute);
     ...
     return realPath;

Because normalizePath strips a trailing `'`, `"`, or whitespace character, a request for /allowed/proj' passes the containment check (normalized to /allowed/proj) while fs.realpath and the returned value operate on /allowed/proj' - a real sibling of the allowed directory.

The post-realpath check runs on normalizePath(realPath), which strips the same character again, so it also passes.

## Affected tools

Every tool that routes through validatePath in src/filesystem:

read_file, read_text_file, read_multiple_files, read_media_file,
write_file, edit_file, create_directory,
list_directory, list_directory_with_sizes, directory_tree,
move_file, search_files, get_file_info,
list_allowed_directories (no path input, not affected in practice)

## Impact

Read, write, move, and stat operations can be performed on paths outside the configured allowed directories, provided a sibling path with a trailing quote/whitespace exists. An attacker who can create such a sibling via the write tools can chain a write plus a read to escape the sandbox.

### Proof of concept

## Environment

- Server: @modelcontextprotocol/server-filesystem (filesystem server, src/filesystem)
- Node.js: 20.x
- Commit tested: latest main as of 2026-10-14

## Reproduction

The following TypeScript test imports the real validatePath from the server's compiled output and demonstrates the escape. Run it from within src/filesystem after `npm install`:

```
  import { validatePath } from "./lib.js";
  import fs from "fs/promises";
  import path from "path";
  import os from "os";

  (async () => {
    const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "mcp_real_"));
    const allowedProj = path.join(sandbox, "allowed/proj");
    const siblingEvil = path.join(sandbox, "allowed/proj'");

    await fs.mkdir(allowedProj, { recursive: true });
    await fs.mkdir(siblingEvil, { recursive: true });
    await fs.writeFile(path.join(siblingEvil, "SECRET.txt"), "OUTSIDE");

    const validated = await validatePath(sandbox + "/allowed/proj'", [allowedProj]);
    // validated === sandbox + "/allowed/proj'" - OUTSIDE the allowlist

    const content = await fs.readFile(path.join(validated, "SECRET.txt"), "utf-8");
    // content === "OUTSIDE" - the escape succeeded

    await fs.rm(sandbox, { recursive: true, force: true });
  })();
```

Run with: npx tsx verify_escape.ts

## Observed output

  validatePath returned: /tmp/mcp_real_y5nErv/allowed/proj'
  🚨 SECRET READ: This file is OUTSIDE the allowlist

The returned path is outside the allowlist, and reading from it succeeded.

### Impact

An attacker who can influence the arguments of a filesystem tool (for example via prompt injection in a document, webpage, or codebase the LLM is reading) can cause read, write, move, and stat operations to occur on files outside the configured allowed directories.

- Confidentiality: read of files outside the sandbox if a sibling path with a trailing quote/whitespace exists.
- Integrity: write and move of files outside the sandbox under the same precondition.
- Chaining: the write tools can create the sibling path, making the precondition trivially satisfiable.

This bypasses the boundary the server claims to enforce ("Only works within allowed directories").

## Upgrading from a date-versioned release

`@modelcontextprotocol/server-filesystem` moved from date versions (2025.x, 2026.x) to semantic versions in 1.0.0. Package managers rank 2.0.1 below every 2025.x and 2026.x version, so they will not offer it as an upgrade and Dependabot will not open one. If you are on a date version, install the fix explicitly: `npm install @modelcontextprotocol/server-filesystem@2.0.1` (or `@latest`).

---

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