Skip to content

fix(filesystem): reject Windows-style paths on POSIX hosts instead of writing literal filenames - #4689

Open
Parker-Fawcett wants to merge 1 commit into
modelcontextprotocol:mainfrom
Parker-Fawcett:fix/reject-windows-paths-on-posix
Open

fix(filesystem): reject Windows-style paths on POSIX hosts instead of writing literal filenames#4689
Parker-Fawcett wants to merge 1 commit into
modelcontextprotocol:mainfrom
Parker-Fawcett:fix/reject-windows-paths-on-posix

Conversation

@Parker-Fawcett

Copy link
Copy Markdown

Description

On a POSIX host, validatePath() accepts Windows drive-letter paths like C:\Users\me\notes\file.md as if they were relative paths. path.resolve(allowedDir, requested) then places them inside the allowed root as a single entry literally named C:\Users\me\notes\file.md — or, for C:/Users/me/..., creates an actual C:/Users/... directory tree. The tool answers "Successfully wrote to ...", so the mistake is only discovered when inspecting disk.

validatePath() now rejects strings matching a Windows drive-letter form (^[A-Za-z]:(?:[\\/]|$)) up front whenever process.platform !== 'win32', with an explicit error:

Access denied - Windows-style path received on a POSIX host: C:\Users\me\notes\file.md

Fixes #4686.

Server Details

  • Server: filesystem
  • Changes to: path validation (validatePath in src/filesystem/lib.ts) + tests

Motivation and Context

Reported in #4686 with a full root-cause analysis: on POSIX path.isAbsolute("C:\\...") is false, so the string falls into resolveRelativePathAgainstAllowedDirectories() and resolves inside the sandbox. Nothing escapes containment — isPathWithinAllowedDirectories() is working correctly — but the caller's intent is silently misinterpreted, and the resulting file is easy to overlook (the reporter's sat unnoticed for three weeks).

The guard is deliberately one-directional and platform-guarded:

  • Only active when process.platform !== 'win32', so Windows behavior is untouched.
  • POSIX paths are never rewritten or normalized into Windows form, avoiding the mirror-image regression described in Docker Image 1.0.2 Breaks Linux Users with Letter at Start of Path #3628.
  • The rejection happens before any filesystem access and before expandHome, failing fast with an actionable message.

How Has This Been Tested?

  • New tests in src/filesystem/__tests__/lib.test.ts (POSIX-only via describe.skipIf(process.platform === 'win32'), following the file's existing platform-conditional style):
    • Rejects C:\Users\me\notes\file.md, C:/Users/me/file.md, Z:\, and bare C: with the explicit error.
    • Rejection occurs before any filesystem access (realpath never called).
    • Regression guard: relative paths that merely contain a colon after the first character (notes/file:C.md) still resolve normally on all platforms.
  • Full suite green: filesystem 158/158 (152 pre-existing + 6 new).
  • Live verification against the real filesystem on macOS: all four drive-letter forms now throw the new error; ordinary absolute and relative paths are unaffected.

Breaking Changes

Behavioral change only for inputs that were already broken in intent: passing a Windows path to a server running on macOS/Linux previously produced a misleading success plus a junk file; it now produces an immediate, descriptive error. No valid workflow is affected, and no client configuration changes are needed.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly (N/A — error-path hardening, no tool surface change)
  • I have tested this with an LLM client (verified via direct validatePath invocation over the built server module plus unit suites; N/A granularity for a validation-layer fix)
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options (N/A)

Additional context

The regex intentionally follows the form proposed in #4686 (^[A-Za-z]:[\\/] plus bare ^[A-Za-z]:$, combined here as ^[A-Za-z]:(?:[\\/]|$)). Known trade-off, accepted per that issue's spec: a POSIX filename that genuinely begins with a single letter followed by a colon (e.g. a:b/c) will now be rejected on POSIX hosts; such names are vanishingly rare in practice and were previously indistinguishable from this bug class anyway.

Happy to follow up with the same treatment for UNC forms (\\server\share) on POSIX if maintainers want them rejected too (#3527 covers the Windows side of UNC handling).

… writing literal filenames

On POSIX, a path like C:\Users\me\notes\file.md is not absolute, so validatePath() sent it down the relative-path branch and path.resolve() placed it inside the allowed root as a single entry literally named 'C:\Users\me\notes\file.md' (or created a C:/ directory tree). Callers saw success and only discovered the mistake when inspecting disk - reported in modelcontextprotocol#4686.

validatePath() now rejects drive-letter forms (^[A-Za-z]:(?:[\\/]|$)) up front when process.platform is not win32, with an explicit access-denied error. The check is one-directional and platform-guarded so Windows behavior is untouched and POSIX paths are never rewritten (no modelcontextprotocol#3628 regression).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
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.

filesystem: a Windows-style path is silently accepted on POSIX and becomes a literal filename inside the allowed root

1 participant