feat: opt-in CODEGRAPH_ALLOW_SYMLINK_ESCAPE for symlink-farm roots - #3
Merged
Merged
Conversation
validatePathWithinRoot rejects in-repo symlinks whose real target escapes the project root (colbymchenry#527). That blocks consolidated multi-repo/worktree workspaces built from symlinks (e.g. a 'pit' dir linking out to sibling repos) — every read is denied and nothing indexes. Add an opt-in env flag that relaxes ONLY the symlink-realpath layer; the lexical '../' traversal guard still applies. Default behavior (flag unset) is unchanged, so colbymchenry#527 stays closed for untrusted repos. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds an explicit opt-in escape hatch to validatePathWithinRoot so that symlink-farm style workspaces (where in-repo symlinks intentionally point to out-of-tree checkouts) can be indexed without being blocked by the symlink-realpath containment guard introduced for colbymchenry#527.
Changes:
- Add
CODEGRAPH_ALLOW_SYMLINK_ESCAPEbehavior to bypass the realpath-based “symlink escape” rejection while keeping the lexical../traversal guard intact. - Add new security tests covering the opt-in behavior for file and directory symlink escapes and confirming lexical traversal remains blocked.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/utils.ts | Adds an env-flag-controlled bypass for the symlink-realpath containment check in validatePathWithinRoot. |
| tests/security.test.ts | Adds test cases validating the new opt-in symlink escape behavior and ensuring ../ traversal is still rejected. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
116
to
+119
| const realRoot = fs.realpathSync(normalizedRoot); | ||
| const realResolved = fs.realpathSync(resolved); | ||
| return isWithinDir(realResolved, realRoot) ? realResolved : null; | ||
| if (isWithinDir(realResolved, realRoot)) return realResolved; | ||
| return process.env.CODEGRAPH_ALLOW_SYMLINK_ESCAPE ? realResolved : null; |
Comment on lines
+258
to
+277
| describe('with CODEGRAPH_ALLOW_SYMLINK_ESCAPE opt-in', () => { | ||
| afterEach(() => { delete process.env.CODEGRAPH_ALLOW_SYMLINK_ESCAPE; }); | ||
|
|
||
| it('allows an in-repo symlink to an out-of-root FILE when opted in', () => { | ||
| if (!link(path.join(root, 'escape'), path.join(outside, 'pkg', 'secret.txt'))) return; | ||
| process.env.CODEGRAPH_ALLOW_SYMLINK_ESCAPE = '1'; | ||
| expect(validatePathWithinRoot(root, 'escape')).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('allows escape through an out-of-root DIR symlink when opted in', () => { | ||
| if (!link(path.join(root, 'escapedir'), path.join(outside, 'pkg'))) return; | ||
| process.env.CODEGRAPH_ALLOW_SYMLINK_ESCAPE = '1'; | ||
| expect(validatePathWithinRoot(root, 'escapedir/secret.txt')).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('still rejects lexical ../ traversal even when opted in', () => { | ||
| process.env.CODEGRAPH_ALLOW_SYMLINK_ESCAPE = '1'; | ||
| expect(validatePathWithinRoot(root, `../${path.basename(outside)}/pkg/secret.txt`)).toBeNull(); | ||
| }); | ||
| }); |
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.
Problema
validatePathWithinRoot(colbymchenry#527) rejeita symlinks in-repo cujo alvo real escapa do root do projeto. Isso quebra workspaces consolidados multi-repo/worktree feitos de symlinks (ex.: um dirpitlinkando pra repos irmãos): toda leitura é bloqueada e nada indexa (0 nós).Solução
Flag opt-in
CODEGRAPH_ALLOW_SYMLINK_ESCAPE=1que afrouxa apenas a camada de symlink-realpath. O guard lexical../continua ativo. Comportamento default (flag off) inalterado — colbymchenry#527 segue fechado pra repos não confiáveis.Testes
__tests__/security.test.ts(TDD red→green).🤖 Generated with Claude Code