fix: prevent path traversal in walkDirectory targetPath (CWE-22)#33
Open
sebastiondev wants to merge 1 commit intoForLoopCodes:mainfrom
Open
fix: prevent path traversal in walkDirectory targetPath (CWE-22)#33sebastiondev wants to merge 1 commit intoForLoopCodes:mainfrom
sebastiondev wants to merge 1 commit intoForLoopCodes:mainfrom
Conversation
|
@sebastiondev is attempting to deploy a commit to the ForLoopCodes' projects Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
walkDirectory()insrc/core/walker.tsaccepts a caller-suppliedtargetPaththat is joined torootDirviaresolve()without any containment check. Becauseresolve()happily collapses..segments and follows symlinks, an MCP client (or a prompt-injected agent driving one) can pass values like"../../.."or a symlink that points outside the project root and have the walker enumerate arbitrary directories on the host. The resulting file list — including header/symbol extracts produced by downstream tools — is returned to the MCP client, giving an attacker arbitrary filesystem read/enumeration scoped only by the OS permissions of the server process.walkDirectoryinsrc/core/walker.tscontext_treeMCP tool propagates the user-controlledtarget_pathargument intowalkDirectory({ rootDir, targetPath }). Other callers passtargetPath: undefinedand are unaffected, but a single fix at the chokepoint covers them all.Data flow
Fix
In
walkDirectory:realpath()bothrootDirand the resolvedstartDirso symlink targets are evaluated, not the link names.relative(rootRealPath, startRealPath)and reject if the result is absolute or starts with..(i.e. anything outside the root).[], so the MCP client gets a clear error instead of an empty/misleading result.loadIgnoreRulesandwalkRecursiveso ignore matching is consistent with the canonicalised tree.The check is implemented as a small helper:
This is the standard Node pattern for path containment and correctly handles the three classes of bypass we tested (
..traversal, sibling directories sharing a name prefix, and symlinks pointing outside the root).Tests
Added three regression tests in
test/main/walker.test.mjs:rejects targetPath traversal outside root—targetPath: ".."rejects targetPath traversal to sibling paths with shared prefixes— guards against a naïvestartsWith(rootDir)check (e.g./tmp/fixturesvs/tmp/fixtures-sibling).rejects symlink escapes passed as targetPath— creates a symlink inside the fixture pointing outside, confirmsrealpathresolution catches it.Full test suite: 218 tests passing, including the three new ones. No existing behaviour changed for legitimate in-root
targetPathvalues (verified by the existing walker tests).Why this is exploitable
Preconditions are minimal:
ROOT_DIR. This is true by default: nothing in the project sandboxes the Node process, so anything the user account can read is reachable.There is no auth layer between the MCP transport and the tool handler, no allowlist on
target_path, and no canonicalisation prior to this fix. The pre-fixstat(startDir)only checks existence; it does not constrain location.Adversarial review
Before submitting we tried to talk ourselves out of this finding. The two plausible mitigations would be (a) framework-level path scoping in the MCP SDK, or (b) the OS limiting what the process can read. Neither applies: the MCP SDK does not inspect tool arguments for filesystem semantics, and the server is intended to run as the developer's user, which is exactly the account with access to interesting files (SSH keys, env files, source trees of other projects). We also confirmed
context_treeis the only caller that forwards user-controlledtargetPath, so this isn't shadowed by a sibling sink that would make the fix moot.Note:
src/tools/static-analysis.tshas a separatetargetPathsink that flows into a linter subprocess. That's a distinct issue (different sink, different CWE class) and is intentionally out of scope here — this PR is deliberately surgical.cc @lewiswigmore