Skip to content

Comments

fix(filesystem): resolve symlinked allowed directories to both forms#3254

Merged
olaservo merged 1 commit intomodelcontextprotocol:mainfrom
wingding12:fix/filesystem-macos-symlink-path-resolution
Feb 19, 2026
Merged

fix(filesystem): resolve symlinked allowed directories to both forms#3254
olaservo merged 1 commit intomodelcontextprotocol:mainfrom
wingding12:fix/filesystem-macos-symlink-path-resolution

Conversation

@wingding12
Copy link
Contributor

@wingding12 wingding12 commented Jan 26, 2026

Summary

Fixes #3253

Problem
On macOS, /tmp is a symlink to /private/tmp. When users specify /tmp as an allowed directory:

  1. Server startup resolves /tmp to /private/tmp and stores only /private/tmp in allowedDirectories
  2. When user requests /tmp/file.txt, the initial validation check fails because /tmp doesn't match /private/tmp
  3. User gets "Access denied - path outside allowed directories" error

Solution
Store both the original normalized path andthe resolved path in allowedDirectories. This allows users to access files through either form:

  • /tmp/file.txt matches /tmp in allowedDirectories
  • /private/tmp/file.txt matches /private/tmp in allowedDirectories

Changes

  • src/filesystem/index.ts: Modified allowed directory initialization to store both original and resolved paths when they differ
  • src/filesystem/__tests__/path-validation.test.ts: Added test for the symlink behavior

Tests

  • Added new test "allows paths through both original and resolved symlink directories"
  • Verified fix works on macOS by observing both paths in allowedDirectories output

Reproduction

Before fix:

npx -y @modelcontextprotocol/server-filesystem /tmp
# Then request /tmp/test.txt -> REJECTED

After fix:

npx -y @modelcontextprotocol/server-filesystem /tmp
# Server logs: allowed directories: ['/tmp', '/private/tmp']
# Then request /tmp/test.txt -> ACCEPTED

On macOS, /tmp is a symlink to /private/tmp. When users specify /tmp
as an allowed directory, the server was resolving it to /private/tmp
during startup but then rejecting paths like /tmp/file.txt because
they dont start with /private/tmp.

This fix stores BOTH the original normalized path AND the resolved
path in allowedDirectories, so users can access files through either
form. For example, with /tmp as allowed directory, both /tmp/file.txt
and /private/tmp/file.txt will now be accepted.

Fixes modelcontextprotocol#3253
Copy link
Member

@olaservo olaservo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: APPROVE with suggestions

Correct fix for a real macOS usability bug. The approach of storing both the original symlink path and the resolved realpath in allowedDirectories is sound and security-neutral.

Bug analysis

The root cause is in validatePath() (lib.ts:108-111): it checks the lexically-normalized request path against allowedDirectories before calling fs.realpath(). When startup only stores the resolved form (/private/tmp), a request for /tmp/file.txt fails the pre-realpath gate and never reaches the second check.

Storing both forms fixes this cleanly at the source — the startup resolution step — without touching the runtime validation logic.

Verified

  • validatePath() pre-realpath gating (lib.ts:108-111) confirmed
  • isPathWithinAllowedDirectories is pure sync prefix-match, no fs I/O
  • normalizePath is purely lexical
  • Security properties unchanged: both stored paths resolve to the same physical directory; symlink-target validation in validatePath() remains intact
  • No conflict with #3229, #3238, or #3205

Suggestions (non-blocking)

  1. Integration test gap: The test in path-validation.test.ts manually constructs allowedDirsWithBoth — it tests isPathWithinAllowedDirectories given correct input, but doesn't verify the startup code in index.ts actually produces both forms. Consider adding a symlink case to startup-validation.test.ts that spawns the server with a symlinked directory argument.

  2. Code comment on catch block: When a directory doesn't exist at startup, only the unresolved path is stored. If it later appears as a symlink, the mismatch could recur for that dir. A brief comment noting this acceptable tradeoff would help future maintainers.


Disclosure: This review was conducted with assistance from Claude Code (claude-opus-4-6). All claims were verified against the source files.

@olaservo olaservo merged commit a83b145 into modelcontextprotocol:main Feb 19, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server-filesystem: macOS /tmp symlink causes path rejection when /tmp is in allowed directories

2 participants