|
|
@@ -0,0 +1,64 @@
|
|
|
+/**
|
|
|
+ * `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);
|
|
|
+ });
|
|
|
+});
|