fix(security): unify path validation to use realpath, removing dead isPathWithinRootReal#384
Open
matrix9neonebuchadnezzar2199-sketch wants to merge 3 commits into
Conversation
Document tool architecture, Cursor integration effects, quality/i18n design, and feature proposals for the codegraph-jp fork effort. Co-authored-by: Cursor <cursoragent@cursor.com>
Consolidate validatePathWithinRoot with realpath symlink checks; remove unused isPathWithinRootReal; add seven security tests. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
I just randomly scanned this PR with a tool I'm building - this is the first PR I've done. I hope it helps with the review. https://www.lenzon.ai/viewer/cmpkhwzw10000138ywdsk076u?voice=google-neural2 |
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
Consolidates two path-validation helpers in
src/utils.tsso that all callerstransparently get
realpath-based symlink-escape protection. The public APIis preserved; only the internals of
validatePathWithinRootchange.This PR also includes fork-only HTML docs under docs/; the security fix is src/utils.ts + tests/security.test.ts only.
Problem
src/utils.tsdefines two helpers with overlapping intent:validatePathWithinRoot(projectRoot, filePath)— logical check only(
path.resolve+ prefix comparison)isPathWithinRootReal(filePath, rootDir)— same logical check, plus afs.realpathSyncstep to catch symlink escapesEvery production call site uses the logical-only variant:
src/mcp/tools.ts(all MCP tool handlers)src/extraction/index.ts(parser entry points)src/context/*(context builder)isPathWithinRootRealis defined and exported but never imported anywherein the repo —
git grep isPathWithinRootRealreturns zero call sites. Thestronger check has been dead code since it was added.
As a result, a symlink inside the project tree whose target is outside the
project root passes validation: the logical path stays under the root, so
the prefix check succeeds, even though the on-disk resolution escapes the
project. There is no test today that exercises this scenario — the
__tests__/security.test.ts"Path Traversal Prevention" section only coversrelative-path traversal and a missing-id case.
Change
src/utils.ts:validatePathWithinRootnow performs the logical check and afs.realpathSyncstep. Resolved real paths are compared with a newcase-aware helper so Windows roots (
C:\Projvsc:\proj) comparecorrectly on case-insensitive filesystems.
ENOENTfromrealpathSyncfalls back to the logical result. This isintentional: indexing legitimately references files that may not exist
on disk at validation time (deletions, race with the watcher). The
logical prefix has already been verified at this point, so the
attack surface for ENOENT is bounded to in-root paths.
realpathSyncerrors (ELOOP,EACCES, etc.) are treatedconservatively as rejections.
isPathWithinRootbecomes a thin boolean wrapper aroundvalidatePathWithinRoot.isPathWithinRootRealis removed (verified dead).__tests__/security.test.ts:Seven new cases under a new "Path Traversal Prevention — symlink escapes"
block:
..traversal rejectedTests creating symlinks early-return on
EPERM/ENOSYS, so Windows runnerswithout developer mode are not broken; they skip 2 of the 7 cases cleanly.
Compatibility
validatePathWithinRoot,isPathWithinRootkeep their signatures and return types. Callers that gate on
!== null/ boolean continue to behave identically for previouslyaccepted paths.
now fail are exactly the symlink-escape cases the helper was nominally
supposed to block.
callers store this in the DB and pass it to
fs.readFile, both of whichare unaffected (and arguably more consistent now that symlinked indirections
are normalized).
package.jsontouch.(
mcp/tools.ts,extraction/index.ts,context/*) keep working withoutedits.
Verification
npm run build # clean npm test # full suite: see note below vitest run tests/security.test.ts # 44 passed / 2 skipped (the 2 are Windows-symlink # cases that early-return without admin privileges) git grep isPathWithinRootReal # 0 hits
In the fork's CI on Windows, 5 unrelated tests in
mcp-roots.*fail withEBUSY/ timeout. These reproduce onmainof this repo prior to thepatch and are not introduced by this PR. They appear consistent with the
known stdin-close →
process.exit(0)race insrc/mcp/transport.ts,which I'll address in a separate PR.
Out of scope
Deliberately not bundled here, to keep the diff focused and reviewable:
install.sh/install.ps1SHA256 verification (the npm-shim pathalready verifies SHA256SUMS since 0.9.4; only the shell direct-install
path is unguarded)
process.exit(0)issue mentioned above)src/mcp/tools.tsdecomposition (currently ~100 KB, single file)Happy to send follow-ups for any of these if the direction is welcome.
Diff size
src/utils.ts +92 -38 tests/security.test.ts +103 -0
Author note: Patch developed against a fork where I'm keeping a code
review log; I'm happy to omit or relocate any of that on merge. The
substantive change is fully contained in the two files above.