fix(resolution): keep the existence probe inside the project root (#1631)
Fixes #1631. Rebased contributor PR #1632 onto main (post-#1749). Lexical containment for `fileExists` filesystem fallback via `lexicalPathWithinRoot`; #935 in-root symlink behaviour preserved.
This commit is contained in:
+13
-2
@@ -26,6 +26,7 @@ import { loadProjectAliases, type AliasMap } from './path-aliases';
|
||||
import { loadGoModule, type GoModule } from './go-module';
|
||||
import { loadWorkspacePackages, type WorkspacePackages } from './workspace-packages';
|
||||
import { logDebug } from '../errors';
|
||||
import { lexicalPathWithinRoot } from '../utils';
|
||||
import type { ReExport } from './types';
|
||||
import { LRUCache } from './lru-cache';
|
||||
|
||||
@@ -538,8 +539,18 @@ export class ReferenceResolver {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Fall back to filesystem for files not yet indexed
|
||||
const fullPath = path.join(this.projectRoot, filePath);
|
||||
// Fall back to filesystem for files not yet indexed. `path.join` does
|
||||
// not clamp, and relative-import resolution hands us paths carrying
|
||||
// `../` segments, so the probe has to be contained (#1631): a path
|
||||
// outside the root can never be an indexed project file, and the
|
||||
// `knownFiles` check above already answered for everything that is.
|
||||
// Lexical containment only: this is a per-candidate hot path, and the
|
||||
// symlink half of `validatePathWithinRoot` costs two `realpathSync`
|
||||
// calls per probe (~70x slower here). It would also be wrong to apply
|
||||
// — indexing deliberately follows in-root symlinks whose targets live
|
||||
// outside the root (#935), so only the `../` escape is refused.
|
||||
const fullPath = lexicalPathWithinRoot(this.projectRoot, filePath);
|
||||
if (fullPath === null) return false;
|
||||
try {
|
||||
return fs.existsSync(fullPath);
|
||||
} catch (error) {
|
||||
|
||||
+21
-4
@@ -80,6 +80,24 @@ function isWithinDir(child: string, parent: string): boolean {
|
||||
return c === p || c.startsWith(p + path.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* The lexical half of {@link validatePathWithinRoot}, on its own.
|
||||
*
|
||||
* Returns the resolved absolute path when `filePath` stays inside
|
||||
* `projectRoot` after `../` segments are applied, or null when it escapes.
|
||||
* No filesystem access — for callers on a hot path that only need to refuse a
|
||||
* lexical escape, and for which the realpath half would be both unnecessary
|
||||
* and far too expensive (the existence probe in resolution's `fileExists`,
|
||||
* #1631: two `realpathSync` calls per probe made it ~70x slower).
|
||||
*
|
||||
* This is NOT a substitute for `validatePathWithinRoot` on any path whose
|
||||
* contents get served — those must keep the symlink-aware check (#527).
|
||||
*/
|
||||
export function lexicalPathWithinRoot(projectRoot: string, filePath: string): string | null {
|
||||
const resolved = path.resolve(projectRoot, filePath);
|
||||
return isWithinDir(resolved, path.resolve(projectRoot)) ? resolved : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a file path stays within the project root, resolving symlinks.
|
||||
*
|
||||
@@ -112,14 +130,13 @@ export function validatePathWithinRoot(
|
||||
filePath: string,
|
||||
options?: { allowSymlinkEscape?: boolean }
|
||||
): string | null {
|
||||
const resolved = path.resolve(projectRoot, filePath);
|
||||
const normalizedRoot = path.resolve(projectRoot);
|
||||
|
||||
// 1. Lexical containment — cheap, catches `../` traversal. Applies even on
|
||||
// the indexing read path: a crafted `../` escape is still rejected.
|
||||
if (!isWithinDir(resolved, normalizedRoot)) {
|
||||
const resolved = lexicalPathWithinRoot(projectRoot, filePath);
|
||||
if (resolved === null) {
|
||||
return null;
|
||||
}
|
||||
const normalizedRoot = path.resolve(projectRoot);
|
||||
|
||||
// 2. Symlink-aware containment — resolve symlinks on both sides and re-check,
|
||||
// so an in-repo symlink whose real target escapes the root is rejected.
|
||||
|
||||
Reference in New Issue
Block a user