fix(sandbox): reject overlapping Windows temp roots

This commit is contained in:
Tianyi Cui
2026-08-10 17:54:48 +08:00
parent 1762d2b3d7
commit 9435ca62a6
16 changed files with 167 additions and 16 deletions

View File

@@ -0,0 +1,53 @@
/** Canonical path-overlap checks that keep workspace and temp capabilities separate. */
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { assertPrivateTempDisjoint, assertTempRootOutsideWorkspace } from '../src/path-boundary.ts'
describe('Windows ACL temp path boundary', () => {
const scratchDirs: string[] = []
afterEach(() => {
for (const dir of scratchDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
function scratch(): string {
const dir = mkdtempSync(join(tmpdir(), 'dsh-acl-boundary-'))
scratchDirs.push(dir)
return dir
}
it('rejects a temp root equal to or below the workspace', () => {
const workspace = scratch()
const nested = join(workspace, 'temp')
mkdirSync(nested)
expect(() => assertTempRootOutsideWorkspace(workspace, workspace)).toThrow(/temp root must be outside the workspace/u)
expect(() => assertTempRootOutsideWorkspace(workspace, nested)).toThrow(/temp root must be outside the workspace/u)
})
it('accepts a temp parent above the workspace because a fresh child is a sibling', () => {
const tempRoot = scratch()
const workspace = join(tempRoot, 'workspace')
mkdirSync(workspace)
expect(() => assertTempRootOutsideWorkspace(workspace, tempRoot)).not.toThrow()
})
it('requires an actual private temp directory to be disjoint in either direction', () => {
const root = scratch()
const workspace = join(root, 'workspace')
const nestedTemp = join(workspace, 'temp')
const siblingTemp = join(root, 'sibling-temp')
mkdirSync(workspace)
mkdirSync(nestedTemp)
mkdirSync(siblingTemp)
expect(() => assertPrivateTempDisjoint([workspace], nestedTemp)).toThrow(/must be disjoint/u)
expect(() => assertPrivateTempDisjoint([nestedTemp], workspace)).toThrow(/must be disjoint/u)
expect(() => assertPrivateTempDisjoint([workspace], siblingTemp)).not.toThrow()
})
})