Fixes #1631. Rebased contributor PR #1632 onto main (post-#1749). Lexical containment for `fileExists` filesystem fallback via `lexicalPathWithinRoot`; #935 in-root symlink behaviour preserved.
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
/**
|
|
* `fileExists` must not probe outside the project root (#1631).
|
|
*
|
|
* `resolveRelativeImport` hands this callback paths built with
|
|
* `path.relative(projectRoot, basePath)`, which can carry `../` segments, and
|
|
* `path.join` does not clamp — so a crafted relative import in an indexed file
|
|
* made the resolver stat arbitrary absolute paths. Nothing outside is read (the
|
|
* content sinks are guarded separately, #527) and no edge is produced, but the
|
|
* probe itself is an existence oracle driven by repository content.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { ReferenceResolver } from '../src/resolution';
|
|
import type { QueryBuilder } from '../src/db/queries';
|
|
|
|
describe('fileExists containment (#1631)', () => {
|
|
let sandbox: string;
|
|
let projectRoot: string;
|
|
|
|
/** The resolver only needs a project root here — `fileExists` never queries. */
|
|
const contextFor = (root: string) =>
|
|
new ReferenceResolver(root, {} as unknown as QueryBuilder).getResolutionContext();
|
|
|
|
beforeEach(() => {
|
|
sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
|
|
projectRoot = path.join(sandbox, 'proj');
|
|
fs.mkdirSync(path.join(projectRoot, 'src'), { recursive: true });
|
|
fs.writeFileSync(path.join(projectRoot, 'src', 'a.js'), 'export const a = 1;');
|
|
// A real file two levels above the root, as the reproduction in #1631 has.
|
|
fs.mkdirSync(path.join(sandbox, 'outside'), { recursive: true });
|
|
fs.writeFileSync(path.join(sandbox, 'outside', 'secret.js'), 'export const secret = 42;');
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(sandbox, { recursive: true, force: true });
|
|
});
|
|
|
|
it('still reports files inside the root', () => {
|
|
expect(contextFor(projectRoot).fileExists('src/a.js')).toBe(true);
|
|
expect(contextFor(projectRoot).fileExists('src/missing.js')).toBe(false);
|
|
});
|
|
|
|
it('refuses to probe a path that escapes the root, even though it exists', () => {
|
|
const escaping = path.join('..', 'outside', 'secret.js');
|
|
// Baseline: the target really is there — so `false` can only come from the guard.
|
|
expect(fs.existsSync(path.join(projectRoot, escaping))).toBe(true);
|
|
|
|
expect(contextFor(projectRoot).fileExists(escaping)).toBe(false);
|
|
});
|
|
|
|
it('keeps following an in-root symlink whose target is outside the root (#935)', () => {
|
|
const link = path.join(projectRoot, 'vendor');
|
|
try {
|
|
fs.symlinkSync(path.join(sandbox, 'outside'), link, 'dir');
|
|
} catch {
|
|
return; // symlink creation not permitted (e.g. Windows without privilege)
|
|
}
|
|
// Lexically inside the root, physically outside — the indexing tier allows this.
|
|
expect(contextFor(projectRoot).fileExists(path.join('vendor', 'secret.js'))).toBe(true);
|
|
});
|
|
});
|