Skip to content

Fix UNC path validation: cover all corruption sites on Windows#3981

Open
quocnam1 wants to merge 1 commit intomodelcontextprotocol:mainfrom
quocnam1:fix/unc-path-validation-complete
Open

Fix UNC path validation: cover all corruption sites on Windows#3981
quocnam1 wants to merge 1 commit intomodelcontextprotocol:mainfrom
quocnam1:fix/unc-path-validation-complete

Conversation

@quocnam1
Copy link
Copy Markdown

Summary

Fixes #3756 — Windows UNC path validation fails in multiple sites of the filesystem server, not just isPathWithinAllowedDirectories.

This PR supersedes #3921, which patches only isPathWithinAllowedDirectories. Three other sites in the filesystem server also corrupt UNC paths, causing every UNC file operation to fail with "Access denied" on Windows even after #3921 is applied. This PR fixes all four sites together and adds tests for the regressions.

Root cause

On Windows, path.normalize('\\\\server\\share') strips a leading backslash (\\server\share\server\share), and path.resolve then interprets the single-backslash result as drive-relative, producing C:\server\share. Any code path that pushes a UNC through path.resolve corrupts it before validation.

The four corruption sites

# File Site Status upstream
1 path-validation.ts isPathWithinAllowedDirectories Fixed by #3921
2 path-validation.ts normalizePossiblyUNCPath trailing separator Not covered
3 lib.ts validatePath pre-check path.resolve Not covered
4 index.ts bootstrap normalization of allowed directories Not covered

Bug #2 — trailing separator

path.normalize preserves trailing separators on UNC paths on Windows, unlike path.resolve for drive paths. After #3921 strips the leading-backslash issue, normalizedDir + path.sep in the startsWith check produces a double separator that never matches a real file path. Any allowed UNC directory configured with a trailing backslash (the common case: \\server\share\) fails every subdirectory match.

Fix: strip the trailing separator inside normalizePossiblyUNCPath, harmonizing with path.resolve behavior on non-UNC paths.

Bug #3validatePath

validatePath calls path.resolve(expandedPath) on the absolute path before the allowed-directory check. On a UNC input, this corrupts the path to C:\server\share\... before the patched check ever sees it. Every UNC write, stat, edit, or rename operation fails with "Access denied" regardless of #3921.

Fix: skip path.resolve for UNC paths and let normalizePath (which is already UNC-aware in path-utils.ts) handle them.

Bug #4 — bootstrap

Same path.resolve corruption runs at server startup when normalizing every allowed directory before setAllowedDirectories. This means every UNC allowed directory stored in the server's state is already corrupted, independently of runtime request handling.

Fix: same guard as #3 applied in the bootstrap loop.

Tests

  • path-validation.test.ts — 4 new tests for the trailing-separator case (bug Create package for each server #2): file inside, subdirectories, exact match, sibling prefix rejection. All gated behind path.sep === '\\'.
  • lib.test.ts — 6 new tests for validatePath end-to-end with UNC (bug Added SQLite Notes To Do Server #3), using the existing vi.mock('fs/promises') pattern and mockFs.realpath returning the input unchanged. Covers within/exact/nested/outside-directory/different-server/trailing-separator. All gated behind process.platform === 'win32'.
  • Bug Update Puppeteer (Code & README) #4 (bootstrap) — no direct unit test. A proper integration test would require a live SMB share in CI, which GitHub Actions doesn't provide. The bootstrap fix applies the exact same expanded.startsWith('\\\\') guard as validatePath, so the logic is covered by the lib.ts tests and by manual validation on a real Windows + SMB setup.

All 159 existing tests continue to pass. Non-UNC paths retain existing behavior on all platforms (same path.resolve(path.normalize(...)) chain).

Manual validation

Tested on Windows 11 + Node v25 against an SMB share on a Linux server, with the binary built from this branch loaded via node in claude_desktop_config.json:

  • list_directory, read_text_file, write_file, get_file_info, edit_file on \\server\share\... all succeed.
  • Non-UNC paths (C:\Users\...) behavior unchanged.
  • Allowed directories configured with and without trailing backslashes both work.

Before this PR (even with #3921 applied): every UNC write and stat fails with "Access denied", only list_directory succeeds by bypassing validatePath.

Checklist

  • Non-UNC path behavior unchanged (verified by existing tests passing on Linux/macOS/Windows)
  • All fixes gated on \\ prefix — zero impact on Unix paths
  • Tests gated behind path.sep === '\\' or process.platform === 'win32' (no-op on Unix CI)
  • No new dependencies
  • Commit message follows existing style

Happy to split this into multiple commits (one per site + tests) if reviewers prefer that format.

PR modelcontextprotocol#3921 fixes isPathWithinAllowedDirectories for UNC paths, but three
other sites in the filesystem server also corrupt UNC paths via
path.resolve or preserve them incorrectly through path.normalize:

1. path-validation.ts::normalizePossiblyUNCPath
   path.normalize preserves trailing separators on UNC paths on Windows
   (unlike path.resolve for drive paths), so 'normalizedDir + path.sep'
   produces a double separator that startsWith() never matches. Strip
   the trailing separator to harmonize.

2. lib.ts::validatePath
   Calls path.resolve(expandedPath) before the allowed-directory check,
   which on Windows corrupts UNC paths into drive-relative paths
   (\\\\server\\share -> C:\\server\\share). Skip path.resolve for UNC
   paths and let normalizePath handle them.

3. index.ts bootstrap
   Same path.resolve corruption when normalizing allowed directories
   at server startup, before they are stored via setAllowedDirectories.
   Same guard applied.

All non-UNC paths retain existing behavior (path.resolve + normalize).
Tests added:
- 4 tests in path-validation.test.ts for the trailing-separator case
- 6 tests in lib.test.ts for validatePath end-to-end on UNC, gated
  behind process.platform === 'win32' and using the existing fs mock
  pattern
- Bug modelcontextprotocol#3 (bootstrap) has no direct unit test since UNC access in CI is
  not available, but the fix applies the same principle as validatePath
  and is logically covered.

Fixes modelcontextprotocol#3756
Supersedes modelcontextprotocol#3921
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.

isPathWithinAllowedDirectories fails for UNC paths on Windows (network drives)

1 participant