Skip to content

Resolve both operands in isUnderRoot so containment works on Windows - #807

Open
anketpratapsingh wants to merge 1 commit into
masterfrom
fix/806-isunderroot-resolve-both-operands
Open

Resolve both operands in isUnderRoot so containment works on Windows#807
anketpratapsingh wants to merge 1 commit into
masterfrom
fix/806-isunderroot-resolve-both-operands

Conversation

@anketpratapsingh

Copy link
Copy Markdown
Collaborator

Summary

Fixes #806.

isUnderRoot() applied path.resolve() to filePath but only path.normalize() to repoRoot. On Windows resolve() prepends the current drive letter and normalize() does not, so a genuinely contained file compared C:\repo\src\a.ts against \repo and was rejected — turning the containment check red on Windows (npm test exited 1 with 25/26 passing).

The function is duplicated verbatim in both packages; both copies are fixed:

  • packages/travsr-lsif-ts/src/security.ts
  • packages/travsr-lsif-py/src/security.ts
const normalized = pathImpl.normalize(pathImpl.resolve(filePath));
const root = pathImpl.normalize(pathImpl.resolve(repoRoot));   // <- was normalize() only

Per the issue: not exploitable in the current call path (walker.ts always passes a root from resolveRoot(), which is already absolute), but the filter's correctness silently depended on that unstated caller precondition, and it fails in the direction of dropping legitimate files. It now holds for any input, and the doc comment records why.

Cross-platform testing

The fix must hold on Windows, macOS, and Linux. Rather than leaving each platform's behaviour to whichever OS happens to run the suite, the function now takes an optional pathImpl: typeof path = path parameter (production callers never pass it), and new tests inject path.win32 and path.posix explicitly with fully-qualified inputs — so every test run on any host exercises both platforms' semantics deterministically:

  • isUnderRoot: Windows semantics, verified from any host (#806) — drive-letter containment, forward-slash normalization, prefix-escape (project-evil), other-drive rejection, .. traversal collapse
  • isUnderRoot: POSIX semantics, verified from any host (#806) — containment, prefix-escape, .. traversal collapse
  • The pre-existing platform-native tests are kept unchanged: they exercise the default parameter on the real host module (this is the test that was red on Windows before the fix)
Platform How verified
Windows Locally (Win 11, Node v24.15.0): travsr-lsif-ts npm run build + npm test28/28 pass, exit 0 (was 25/26, exit 1). travsr-lsif-py: tsc build clean + 11 assertions against the real compiled dist/security.js (native tree-sitter install needs Python/node-gyp, unavailable on this machine, so the full py suite ran as a direct-import harness instead)
Linux CI: the lsif-ts job on ubuntu-latest runs the full suite including the new injected-flavour tests
macOS No macOS runner exists in CI for these packages. isUnderRoot does zero I/O — it is pure string manipulation over the path module, and Node's path on macOS is byte-for-byte the same POSIX implementation as on Linux (path.posix). That exact flavour is pinned by the injected-path.posix test, which runs on every host, including the Linux CI run of this PR

Notes for reviewers

  • travsr-lsif-py has no CI job (only lsif-ts is wired into ci.yml), so its copy of this fix is not exercised by CI at all — flagging in case a lsif-py job is worth adding while this is fresh.
  • The two packages' security.ts files had already drifted slightly around this function; the issue suggests a shared module as a follow-up, deliberately not done here to keep the fix minimal.

isUnderRoot() applied path.resolve() to filePath but only
path.normalize() to repoRoot. On Windows resolve() prepends the
current drive letter and normalize() does not, so a genuinely
contained file compared "C:\repo\src\a.ts" against "\repo" and was
rejected. The function is duplicated verbatim in travsr-lsif-ts and
travsr-lsif-py; both copies are fixed.

Not exploitable in the current call path (walker.ts always passes a
root from resolveRoot(), which is already absolute), but the red test
made npm test unrunnable on Windows dev machines, and the containment
filter's correctness silently depended on an unstated caller
precondition. It now holds for any input, and the doc comment records
why.

The function takes an optional pathImpl parameter (defaulting to the
host path module) so the tests can pin its Windows AND POSIX
behaviour from any host: new tests inject path.win32 and path.posix
explicitly with fully-qualified inputs, making the platform matrix
part of every test run instead of only surfacing on whichever OS CI
happens to use. Production callers never pass it.

Fixes #806
@github-actions

Copy link
Copy Markdown

Benchmark comparison (master → PR)

no benchmark comparisons to show

Threshold: p95 regression ≥ +10% fails this check.

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.

isUnderRoot resolves filePath but not repoRoot, so containment checks fail on Windows

1 participant