Skip to content

fix: resolve filesystem tool paths symlink-aware before read/write/list - #2880

Closed
dungdong-aws wants to merge 1 commit into
Amazon-Q-Developer:feature/mcp-security-enchancefrom
dungdong-aws:fix/filesystem-tool-path-resolution
Closed

dungdong-aws wants to merge 1 commit into
Amazon-Q-Developer:feature/mcp-security-enchancefrom
dungdong-aws:fix/filesystem-tool-path-resolution

Conversation

@dungdong-aws

@dungdong-aws dungdong-aws commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Problem

The filesystem tools resolve the path they operate on differently from the path the workspace-containment check evaluates.

  • ChecktoolShared.ts's requiresPathAcceptance() calls resolveSymlinkAwarePath(), which follows symlinks at every segment (including a link whose target does not exist yet) and tests the resolved target against the workspace boundary. If the resolved target is inside the workspace it returns requiresAcceptance: false and no prompt is shown.
  • OperationfsWrite, fsReplace, fsRead, fileSearch, and listDirectory called sanitize() instead, which only expands ~ and makes the path absolute. It does not resolve symlinks.

The unresolved path was then passed to writeFile / readFile / the directory walk, so the operating system followed the link when it opened the file. The tool therefore did not operate on the target its own boundary check had evaluated: if the link is replaced while the operation is in flight, the check evaluated one target and the operation landed on another.

A path whose name sits inside the workspace can point anywhere, so this affects all five tools: an out-of-workspace write for fsWrite/fsReplace, out-of-workspace file contents returned to the model for fsRead, and out-of-workspace names disclosed by fileSearch/listDirectory.

Change

Add resolveCanonicalPath() to toolShared.tssanitize() (expand ~, make absolute) followed by resolveSymlinkAwarePath() (follow symlinks at every segment) — and use it in requiresPathAcceptance() and at every I/O site in the five tools.

sanitize() is composed with the resolver rather than replaced by it. resolveSymlinkAwarePath starts at path.resolve() and does not expand ~, so calling it alone at these call sites would drop tilde expansion; composing keeps that behavior and adds symlink resolution.

File Before After
toolShared.ts resolveSymlinkAwarePath(inputPath) resolveCanonicalPath(inputPath)
fsWrite.ts sanitize(params.path) in validate/invoke await resolveCanonicalPath(params.path)
fsReplace.ts sanitize(params.path) in validate/invoke await resolveCanonicalPath(params.path)
fsRead.ts sanitize(path) before readFile await resolveCanonicalPath(path)
fileSearch.ts sanitize(params.path) before the walk await resolveCanonicalPath(params.path)
listDirectory.ts sanitize(params.path) before the listing await resolveCanonicalPath(params.path)

Text alternative for the table: each of the five tools plus the shared helper moves from a non-symlink-aware sanitize() call to the symlink-aware resolveCanonicalPath() at both its validation and its I/O site.

Every call site was already async, so awaiting the resolver required no signature changes.

Testing

  • Formatting/commit hooks pass (pretty-quick, git-secrets, commitlint).
  • Existing unit suites cover all five tools (fsWrite.test.ts, fsReplace.test.ts, fsRead.test.ts, fileSearch.test.ts, listDirectory.test.ts, toolShared.test.ts).
  • Note: these tools now report and act on the resolved target rather than the link path. Assertions comparing a path string where the workspace or temp root is itself a symlink (for example macOS /tmp -> /private/tmp) will see the resolved value; content assertions are unaffected.

Base branch: feature/mcp-security-enchance.

The filesystem tools derived the path they operate on differently from the
path the workspace-containment check evaluates. `requiresPathAcceptance`
resolves symlinks at every segment via `resolveSymlinkAwarePath`, while
`fsWrite`, `fsReplace`, `fsRead`, `fileSearch`, and `listDirectory` called
`sanitize()`, which only expands `~` and makes the path absolute. The
unresolved path was then handed to `writeFile`/`readFile`/the directory
walk, so the operating system followed the link when it opened the file
rather than the tool operating on the target it had evaluated.

Add `resolveCanonicalPath()` to `toolShared` (sanitize, then resolve
symlinks) and use it in both `requiresPathAcceptance` and all five tools,
so the boundary decision and the operation act on the same resolved path
and a link replaced after the path is resolved no longer changes where the
operation lands.

`sanitize()` is composed with the resolver rather than replaced by it:
`resolveSymlinkAwarePath` does not expand `~`, so calling it alone would
drop tilde expansion at these call sites.
@dungdong-aws
dungdong-aws requested a review from a team as a code owner September 18, 2026 06:06
@dungdong-aws
dungdong-aws marked this pull request as draft September 18, 2026 06:11
@dungdong-aws

Copy link
Copy Markdown
Contributor Author

Closed and superseded by #2881.

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.

1 participant