fileExists in src/resolution/index.ts falls back to the filesystem without containing the path:
fileExists: (filePath: string) => {
if (this.knownFiles) { /* … index hit, O(1) … */ }
// Fall back to filesystem for files not yet indexed
const fullPath = path.join(this.projectRoot, filePath);
try { return fs.existsSync(fullPath); } catch { … }
}
resolveRelativeImport computes path.relative(projectRoot, basePath), which can carry ../ segments, and path.join does not clamp — path.join('/root', '../../etc/passwd') is /etc/passwd. So a relative import in an indexed file probes existence outside the project root.
Reproduction
Nine lines, no OpenSCAD involved — this affects every language with relative imports:
mkdir -p esc/proj/src esc/outside
echo 'export function outsideSecret() { return 42; }' > esc/outside/secret.js
printf "import { outsideSecret } from '../../../outside/secret.js';\nexport function useIt() { return outsideSecret(); }\n" > esc/proj/src/a.js
cd esc/proj && git init -q . && git add -A && git -c user.email=t@t -c user.name=t commit -qm init
strace -f -e trace=newfstatat,stat,lstat,access -o /tmp/tr.txt codegraph init -i .
grep -c "outside/secret.js" /tmp/tr.txt
Result: 9 access() calls against paths two levels above the project root.
access(".../esc/outside/secret.js.js", F_OK) = -1 ENOENT
access(".../esc/outside/secret.js.jsx", F_OK) = -1 ENOENT
access(".../esc/outside/secret.js.mjs", F_OK) = -1 ENOENT
…
Only access — no openat for that path, so nothing outside is read.
Severity, stated plainly
Low, and I would rather understate it than dress it up:
What remains is an existence oracle driven by repository content: a .scad/.js/.py file in an indexed repo can make the indexer probe whether an arbitrary absolute path exists, and timing or subsequent behaviour could in principle expose that. It is also just surprising — every other layer contains paths, and this one quietly does not.
Suggested fix
The knownFiles set is consulted first, so any indexed file resolves regardless of where it lives. Only the filesystem fallback needs bounding, and a path outside the root can never be an indexed project file — so returning false there is the correct answer rather than a new restriction:
const fullPath = path.join(this.projectRoot, filePath);
if (!validatePathWithinRoot(this.projectRoot, fullPath, { allowSymlinkEscape: true })) {
return false;
}
allowSymlinkEscape matches the indexing tier established in #935, so an in-root symlink to a vendored library outside the repo keeps working; only the lexical ../ escape is refused.
I have deliberately not included this in #1630 (OpenSCAD support). Fixing it there would give one language a guarantee the other forty lack, and the change belongs to whoever owns this code path rather than to a language PR.
Found while validating OpenSCAD support; confirmed on 6a056ec (v1.6.0).
fileExistsinsrc/resolution/index.tsfalls back to the filesystem without containing the path:resolveRelativeImportcomputespath.relative(projectRoot, basePath), which can carry../segments, andpath.joindoes not clamp —path.join('/root', '../../etc/passwd')is/etc/passwd. So a relative import in an indexed file probes existence outside the project root.Reproduction
Nine lines, no OpenSCAD involved — this affects every language with relative imports:
Result: 9
access()calls against paths two levels above the project root.Only
access— noopenatfor that path, so nothing outside is read.Severity, stated plainly
Low, and I would rather understate it than dress it up:
validatePathWithinRootwork in Bug: validatePathWithinRoot allows reading files outside project root via symlinks #527, so this is not the file-disclosure path that fixed.What remains is an existence oracle driven by repository content: a
.scad/.js/.pyfile in an indexed repo can make the indexer probe whether an arbitrary absolute path exists, and timing or subsequent behaviour could in principle expose that. It is also just surprising — every other layer contains paths, and this one quietly does not.Suggested fix
The
knownFilesset is consulted first, so any indexed file resolves regardless of where it lives. Only the filesystem fallback needs bounding, and a path outside the root can never be an indexed project file — so returningfalsethere is the correct answer rather than a new restriction:allowSymlinkEscapematches the indexing tier established in #935, so an in-root symlink to a vendored library outside the repo keeps working; only the lexical../escape is refused.I have deliberately not included this in #1630 (OpenSCAD support). Fixing it there would give one language a guarantee the other forty lack, and the change belongs to whoever owns this code path rather than to a language PR.
Found while validating OpenSCAD support; confirmed on
6a056ec(v1.6.0).