resolution-fileexists-containment.test.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. /**
  2. * `fileExists` must not probe outside the project root (#1631).
  3. *
  4. * `resolveRelativeImport` hands this callback paths built with
  5. * `path.relative(projectRoot, basePath)`, which can carry `../` segments, and
  6. * `path.join` does not clamp — so a crafted relative import in an indexed file
  7. * made the resolver stat arbitrary absolute paths. Nothing outside is read (the
  8. * content sinks are guarded separately, #527) and no edge is produced, but the
  9. * probe itself is an existence oracle driven by repository content.
  10. */
  11. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  12. import * as fs from 'fs';
  13. import * as os from 'os';
  14. import * as path from 'path';
  15. import { ReferenceResolver } from '../src/resolution';
  16. import type { QueryBuilder } from '../src/db/queries';
  17. describe('fileExists containment (#1631)', () => {
  18. let sandbox: string;
  19. let projectRoot: string;
  20. /** The resolver only needs a project root here — `fileExists` never queries. */
  21. const contextFor = (root: string) =>
  22. new ReferenceResolver(root, {} as unknown as QueryBuilder).getResolutionContext();
  23. beforeEach(() => {
  24. sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
  25. projectRoot = path.join(sandbox, 'proj');
  26. fs.mkdirSync(path.join(projectRoot, 'src'), { recursive: true });
  27. fs.writeFileSync(path.join(projectRoot, 'src', 'a.js'), 'export const a = 1;');
  28. // A real file two levels above the root, as the reproduction in #1631 has.
  29. fs.mkdirSync(path.join(sandbox, 'outside'), { recursive: true });
  30. fs.writeFileSync(path.join(sandbox, 'outside', 'secret.js'), 'export const secret = 42;');
  31. });
  32. afterEach(() => {
  33. fs.rmSync(sandbox, { recursive: true, force: true });
  34. });
  35. it('still reports files inside the root', () => {
  36. expect(contextFor(projectRoot).fileExists('src/a.js')).toBe(true);
  37. expect(contextFor(projectRoot).fileExists('src/missing.js')).toBe(false);
  38. });
  39. it('refuses to probe a path that escapes the root, even though it exists', () => {
  40. const escaping = path.join('..', 'outside', 'secret.js');
  41. // Baseline: the target really is there — so `false` can only come from the guard.
  42. expect(fs.existsSync(path.join(projectRoot, escaping))).toBe(true);
  43. expect(contextFor(projectRoot).fileExists(escaping)).toBe(false);
  44. });
  45. it('keeps following an in-root symlink whose target is outside the root (#935)', () => {
  46. const link = path.join(projectRoot, 'vendor');
  47. try {
  48. fs.symlinkSync(path.join(sandbox, 'outside'), link, 'dir');
  49. } catch {
  50. return; // symlink creation not permitted (e.g. Windows without privilege)
  51. }
  52. // Lexically inside the root, physically outside — the indexing tier allows this.
  53. expect(contextFor(projectRoot).fileExists(path.join('vendor', 'secret.js'))).toBe(true);
  54. });
  55. });