containment.spec.ts 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. /**
  2. * Containment tests for lexical canonical paths and filesystem-identity aliases.
  3. */
  4. import { afterEach, beforeEach, describe, expect, it } from 'vitest'
  5. import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'
  6. import { tmpdir } from 'node:os'
  7. import { join, parse } from 'node:path'
  8. import { isPathUnder } from '../src/containment.ts'
  9. let base: string
  10. beforeEach(async () => {
  11. base = await mkdtemp(join(tmpdir(), 'dsh-fssbx-containment-'))
  12. })
  13. afterEach(async () => {
  14. await rm(base, { recursive: true, force: true })
  15. })
  16. describe('filesystem sandbox containment', () => {
  17. it('accepts equal paths, descendants, and a filesystem-root boundary', async () => {
  18. expect(await isPathUnder(base, base)).toBe(true)
  19. expect(await isPathUnder(join(base, 'child'), base)).toBe(true)
  20. expect(await isPathUnder(base, parse(base).root)).toBe(true)
  21. })
  22. it('uses case-insensitive lexical comparison for Windows-style containment', async () => {
  23. expect(await isPathUnder(join(base.toUpperCase(), 'child'), base.toLowerCase(), false)).toBe(true)
  24. expect(await isPathUnder(join(base, 'case-sensitive-child'), base, true)).toBe(true)
  25. })
  26. it('recognizes an alias-equivalent root by filesystem identity for a missing target', async () => {
  27. const realRoot = join(base, 'real')
  28. const aliasRoot = join(base, 'alias')
  29. await mkdir(realRoot)
  30. await symlink(realRoot, aliasRoot)
  31. expect(await isPathUnder(join(await realpath(realRoot), 'missing', 'file.txt'), aliasRoot)).toBe(true)
  32. })
  33. it('denies unrelated and missing roots', async () => {
  34. const allowed = join(base, 'allowed')
  35. const outside = join(base, 'outside')
  36. await mkdir(allowed)
  37. await mkdir(outside)
  38. expect(await isPathUnder(join(outside, 'file.txt'), allowed)).toBe(false)
  39. expect(await isPathUnder(join(outside, 'file.txt'), join(base, 'missing-root'))).toBe(false)
  40. })
  41. it('treats a regular-file path segment as a missing target, not containment', async () => {
  42. const allowed = join(base, 'allowed')
  43. const blocker = join(base, 'blocker')
  44. await mkdir(allowed)
  45. await writeFile(blocker, 'not a directory')
  46. expect(await isPathUnder(join(blocker, 'child.txt'), allowed)).toBe(false)
  47. })
  48. })