Skip to content

fix: reject sensitive project paths through symlinks - #1930

Closed
mixxer wants to merge 4 commits into
colbymchenry:mainfrom
mixxer:fix/project-path-realpath
Closed

mixxer wants to merge 4 commits into
colbymchenry:mainfrom
mixxer:fix/project-path-realpath

Conversation

@mixxer

@mixxer mixxer commented Sep 24, 2026 •

Copy link
Copy Markdown

Summary

Validate both the supplied path and its real path before indexing. Reject symlink aliases of sensitive system directories and sensitive home directories, including a symlinked HOME or a .ssh directory that is itself a symlink. Preserve existing handling of paths that do not exist.

POSIX symlink regressions are platform-gated; Windows path validation is checked separately against the upstream baseline.

Validation

  • Rebased on upstream 6560052a6f856855d3f71eee838fd66ccfa4285d; tested head 0f75b0d0e7076fb858fbbddf346cbc47a5754307.
  • macOS / Node 24.21.0: build passed; full suite: 441 passed / 16 skipped files; 5,715 passed, 263 skipped (portable backend).
  • TypeScript language server: no new diagnostics relative to upstream; changed source files have no errors or warnings. Existing upstream test diagnostics are tracked separately.
  • Linux CI: all 11 jobs passed, including this exact PR head on Node 24. Native kernel checks also passed for fix: preserve Java and Kotlin anonymous inheritance #1927 and fix(java): resolve calls through static fields #1949; integrated main passed on Node 22 and 24.
  • Windows path validation: both this head and upstream passed the two applicable focused tests (ordinary directory and case-insensitive Windows system paths). POSIX symlink tests are skipped on Windows.

@inth3shadows inth3shadows left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this on Linux by calling validateProjectPath on symlinks I created. Links to /etc, / and /lib, and chained links, are now refused. Real project dirs, symlinked project dirs, missing paths and symlink loops behave as before.

One gap: the ~/.ssh / .aws / .gnupg / .config checks compare the resolved candidate with the unresolved os.homedir(). If $HOME is itself a symlink (for example /home -> /var/home on Fedora Atomic), a link to ~/.ssh resolves to the real path and is accepted. Repro: HOME=<dir>/homelink, where homelink -> realhome. A link to homelink/.ssh returns null (allowed), and so does realhome/.ssh passed directly; only the literal $HOME/.ssh is refused. unsafeIndexRootReason in src/directory.ts already resolves the home dir. Doing the same here (with a fallback when resolution fails), and adding a test with a symlinked HOME, would close it.

Update (2026-09-26): Verified 448b6a6: a link through a symlinked $HOME is refused now, thanks. One narrower case remains. When ~/.ssh is itself a symlink (as dotfile managers like stow set up), a link to it resolves outside home and is accepted: HOME=h, h/.ssh -> d/ssh-real, link -> h/.ssh → validateProjectPath(link) returns null. Resolving each sensitive home dir as well (realpathSync with the same fallback) and adding a test would close it.

@mixxer

mixxer commented Sep 25, 2026

Copy link
Copy Markdown
Author

Addressed the symlinked $HOME gap in 448b6a6. validateProjectPath now checks sensitive home subdirectories against both the lexical home path and its resolved path, with a fallback if resolving home fails. Added a POSIX regression using a symlinked home. __tests__/security.test.ts: 48 passed, 2 skipped; npm run build: passed.

@mixxer

mixxer commented Sep 25, 2026

Copy link
Copy Markdown
Author

Fork follow-up: mixxer/codegraph-aosp#6 carries the symlinked-home fix on the fork's main. Its Node 22/24, native kernel/AOSP, and final CI checks all passed.

@mixxer

mixxer commented Sep 26, 2026

Copy link
Copy Markdown
Author

I saw the 2026-09-26 update to your review and reproduced the ~/.ssh-itself-is-a-symlink case. The new regression failed before the fix because validateProjectPath(link) returned null.

I pushed 34956d8: the validator now checks both the lexical and resolved location of each sensitive home directory. The regression covers a link through ~/.ssh and the resolved target. On macOS, security.test.ts passed (49 passed, 2 skipped) and npm run build passed. The same change is in fork PR #9, where integrated CI is running. Thanks for the precise repro.

@mixxer

mixxer commented Sep 26, 2026

Copy link
Copy Markdown
Author

Final fork status for the symlinked-sensitive-directory follow-up: mixxer/codegraph-aosp#9 is merged at e58d4aa. The post-merge main CI run passed Node 22, Node 24, native kernel/AOSP, and CI checks. Upstream PR #1930 carries the same fix at 34956d8.

@mixxer
mixxer force-pushed the fix/project-path-realpath branch from 34956d8 to 0f75b0d Compare October 4, 2026 15:30
@mixxer
mixxer force-pushed the fix/project-path-realpath branch 2 times, most recently from 3be0f55 to 576f387 Compare October 22, 2026 05:22
@mixxer
mixxer force-pushed the fix/project-path-realpath branch from 576f387 to 4c8f644 Compare October 24, 2026 03:33
colbymchenry added a commit that referenced this pull request Oct 27, 2026
…s and \?\ paths (#2929)

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>
@colbymchenry

Copy link
Copy Markdown
Owner

Thanks @mixxer for finding this! The sensitive-folder guard really did check only the path as typed, so a link to ~/.ssh, ~/.config or /etc got through. We landed a fix in #2929, reimplemented independently and credited to you in the changelog. It checks both the typed path and the folder it really names, so it also covers different capitalizations on Windows and macOS, \\?\ spellings on Windows, and a missing sub-folder passed as an MCP projectPath. Your three POSIX shapes are among its tests. Closing this one in favor of it; it ships in the next release.

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.

3 participants