Repository navigation
fix(security): the sensitive-folder guard refuses links, case variants and \\?\ paths - #2929
Merged
Merged
Conversation
…s and \\?\ paths The guard that keeps ~/.ssh, ~/.gnupg, ~/.aws, ~/.config and system folders such as /etc and C:\Windows from being opened as a project compared the path as typed. A junction or symlink to one of them, a different capitalization on Windows or macOS (~/.SSH), or a \\?\ or \\.\ spelling on Windows passed, so an MCP projectPath, `codegraph ui` and the viewer's per-file reads all served what was inside. The MCP gate also skipped the guard for a path that doesn't exist and then walked up from it, so ~/.config/no-such-dir reached an index of ~/.config. validateProjectPath now checks the path as spelled (with a Win32 namespace prefix stripped) and its fs.realpathSync.native form. Both are compared with the system set and the system folders' own real paths (macOS /private/etc), and with the home folders as spelled, under the home's real path and by their own real paths (a linked ~/.ssh). Case is folded on win32 and darwin. A path that can't be resolved keeps the lexical check. It stays at one realpath per call: the system folders' real paths are read once and the home folders' are cached for two seconds. The MCP handler also checks the root its up-walk lands on. Validation on Windows 11, with a private home holding a fake token: - Probe: on main 11 of 16 sensitive spellings were allowed, and the token was served through 6 MCP projectPath shapes and 2 viewer roots. With the fix all are refused. Controls (an ordinary folder, its upper-case and \\?\ spellings, the home itself) are still allowed. - codegraph ui end to end: main served the token for 4 of 6 spellings and started the viewer for a fifth; the fix refuses all 6 at startup. - Tests: the 9 new tests that run on Windows fail on main's source and pass with the fix. The POSIX and macOS cases are gated and were not run here. - Cost: validateProjectPath 48 -> 130 us per call, the viewer's per-file resolveProjectFile 700 -> 780 us (medians of 5 rounds, 3 runs). Reported in PR #1930 by @mixxer; this reimplements the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 27, 2026
Merged
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
CodeGraph refuses to open
~/.ssh,~/.gnupg,~/.aws,~/.configand system folders (/etc,C:\Windows, …) as a project. That guard compared the path as typed, so it could be sidestepped:~/.SSH,~/.CONFIG, an upper-cased home);\\?\or\\.\spelling on Windows (\\?\C:\Windows);~/.config/no-such-dir): the guard only runs on a path that exists, and the handler then walks up from it into the folder's own index.The guard gates an MCP
projectPath,codegraph ui, and every file the viewer reads, so each of these served what was inside (verified below with a fake token).Root cause
validateProjectPath(src/utils.ts) checkedpath.resolve(dirPath)only: an exact match against the system set (lower-cased), and a case-sensitivestartsWithagainstpath.join(os.homedir(), dir). Nothing followed links, folded case for the home folders, or understood Win32 namespace prefixes. macOS's/etcis really/private/etc, so the system set never matched a real path there either. Separately,ToolHandler.getCodeGraph(src/mcp/tools.ts) skips the check for a path that doesn't exist (so a not-yet-created sub-path can resolve up to its project, #238) and never checked the root the walk landed on.Fix
validateProjectPathchecks two candidates: the path as spelled (after stripping a\\?\,\\.\or\\?\UNC\prefix), and itsfs.realpathSync.nativeform. The native form follows symlinks, junctions andsubstdrives, and returns the stored case and long names. If the path can't be resolved (most often because it doesn't exist), only the lexical check runs./private/etcon macOS);~/.sshthat is itself a link into a dotfiles checkout).unsafeIndexRootReasonalready does.…\config-junction (resolves to C:\Users\me\.config).getCodeGraphalso runs the guard on the root its up-walk lands on.Every refusal main makes still fires: the old lexical checks run unchanged on the spelled path, which only differs from main's when a namespace prefix was stripped. The existence check still stats the path exactly as main did.
Validation
Windows 11 x64, Node 22.22. A private home (
USERPROFILE/HOMEset to a kit folder) holds a fake~/.configwith aFAKE-TOKENfile and a fake~/.ssh. The fake~/.configis indexed, and a junction and a directory symlink point at the fake folders.Probe (
validateProjectPath,codegraph_nodewith aprojectPath, the viewer'sresolveProjectFile):projectPathshapes that served the token (of 7)\\?\spellings, the home itselfcodegraph uiend to end (CODEGRAPH_UI=1, real CLI; a served viewer was asked for/api/source?file=nvim/init.lua):~/.config~/.CONFIG\\?\+~/.configTests (red→green: main's
src/utils.tsandsrc/mcp/tools.tsput back together, then restored):security.test.ts: links to a sensitive home folder and folders under it; a credential folder that is itself a link; a linked home; case variants (win32 + darwin);\\?\,\\.\and//?/spellings and a junction toC:\Windows(win32); a symlink to/etc(POSIX: this also covers macOS's/private/etcthrough the system set's real paths);/private/etcitself (darwin). Plus an MCP case against a real index: a junction to the folder, a missing folder under it, and a missing folder under the junction must come back as a refusal, with no token in the output.ui-server.test.ts: a viewer root that is a link to a sensitive folder is refused.cli-ui-command.test.ts:codegraph ui <link to ~/.config>is refused.tools.tsalone, with the fixedutils.ts, still fails the missing-sub-folder case, so both changes are needed.Cost (median of 5 rounds × 2,000 calls, 3 runs):
validateProjectPath48 → 130 µs per call; the viewer's per-fileresolveProjectFile700 → 780 µs.Full suite: 760 files passed, 6 skipped; 9,051 tests passed, 0 failed, 85 skipped, with the kernel built (
CODEGRAPH_KERNEL_EXPECT=1) and the daemon allowed. It ran onad277047, this change on4eb8c2a5. After rebasing onto7ff2841e(#2927, resolution files only), I reran this PR's three test files and the three #2927 changed on the pushed commit: 175 passed, 8 skipped.Left out
\\localhost\c$\Users\me\.ssh) still passes.realpathSync.nativereturns UNC paths as they are, and mapping a share back to its local folder needs share enumeration. That needs an admin share or a share the user made of their own folder.C:are not added to the system set; this PR only makes the existing entries hold.Changelog: the agent side is under
[Unreleased]→### Security. Thecodegraph uiside is indocs/viewer-launch-changelog.md, because the viewer isn't released yet (AGENTS.md).Reported in #1930 by @mixxer; this reimplements the fix independently. Thanks to them for finding the link bypass and its POSIX shapes; the Windows case,
\\?\and missing-folder shapes are added here.🤖 Generated with Claude Code