fix(ai-vault): close approved-root traversal race

This commit is contained in:
OrcaWin
2026-08-07 19:46:35 -07:00
parent a65a328d90
commit eb95665fd6
2 changed files with 128 additions and 9 deletions
+119
View File
@@ -0,0 +1,119 @@
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { containedDeleteCommand } from './wsl-contained-delete'
import { parseWslPath } from './wsl'
const execFileAsync = promisify(execFile)
const DISTRO = process.env.ORCA_WSL_TEST_DISTRO ?? 'Ubuntu-24.04'
const runRealWsl = process.platform === 'win32' && process.env.ORCA_REAL_WSL_DELETE_TEST === '1'
function unc(linuxPath: string): string {
return `\\\\wsl.localhost\\${DISTRO}${linuxPath.replaceAll('/', '\\')}`
}
async function wsl(command: string, ...args: string[]): Promise<string> {
const result = await execFileAsync(
'wsl.exe',
['-d', DISTRO, '--exec', 'sh', '-c', command, 'orca-wsl-test', ...args],
{ encoding: 'utf-8', timeout: 30000 }
)
return result.stdout.trim()
}
describe.skipIf(!runRealWsl)('WSL approved-root traversal race', () => {
let fixtureRoot: string
beforeAll(async () => {
fixtureRoot = await wsl("mktemp -d -p /tmp 'orca-wsl-root-race.XXXXXX'")
const statHook = String.raw`#!/bin/sh
for argument do last=$argument; done
if [ "$PWD" = "$ORCA_RACE_PARENT" ] && [ "$(/usr/bin/basename "$last")" = vault ]; then
inspected=$(/usr/bin/stat "$@") || exit $?
/usr/bin/mv -- "$ORCA_RACE_PARENT/vault" "$ORCA_RACE_PARENT/vault-original" || exit $?
/usr/bin/ln -s -- "$ORCA_RACE_OUTSIDE" "$ORCA_RACE_PARENT/vault" || exit $?
printf '%s\n' "$inspected"
exit 0
fi
exec /usr/bin/stat "$@"
`
await wsl(
'mkdir -p "$1/race/vault" "$1/execute-only/vault" "$1/outside/root-target" ' +
'"$1/hook-bin" && ' +
'printf approved > "$1/race/vault/session.json" && ' +
'printf approved > "$1/execute-only/vault/session.json" && ' +
'printf protected > "$1/outside/root-target/session.json" && ' +
'printf unrelated > "$1/outside/unrelated" && ' +
'chmod 111 "$1/execute-only" && ' +
'printf %s "$2" > "$1/hook-bin/stat" && chmod +x "$1/hook-bin/stat"',
fixtureRoot,
statHook
)
})
afterAll(async () => {
if (/^\/tmp\/orca-wsl-root-race\.[A-Za-z0-9]+$/u.test(fixtureRoot)) {
await wsl('rm -rf -- "$1"', fixtureRoot)
}
})
it('fails closed when an approved-root component is replaced after inspection', async () => {
const approvedRoot = `${fixtureRoot}/race/vault`
const target = parseWslPath(unc(`${approvedRoot}/session.json`))
const command = target
? containedDeleteCommand(target, [unc(approvedRoot)], parseWslPath, false)
: null
expect(command).not.toBeNull()
await expect(
execFileAsync(
'wsl.exe',
[
'-d',
DISTRO,
'--exec',
'env',
`PATH=${fixtureRoot}/hook-bin:/usr/bin:/bin`,
`ORCA_RACE_PARENT=${fixtureRoot}/race`,
`ORCA_RACE_OUTSIDE=${fixtureRoot}/outside/root-target`,
...(command ?? [])
],
{ encoding: 'utf-8', timeout: 30000 }
)
).rejects.toMatchObject({ stderr: expect.stringContaining('ORCA_WSL_DELETE_REJECT:race') })
await expect(
wsl(
'test -f "$1/outside/root-target/session.json" && ' +
'test -f "$1/race/vault-original/session.json" && test -f "$1/outside/unrelated"',
fixtureRoot
)
).resolves.toBe('')
})
it('allows an approved root beneath an execute-only ancestor', async () => {
const approvedRoot = `${fixtureRoot}/execute-only/vault`
const target = parseWslPath(unc(`${approvedRoot}/session.json`))
const command = target
? containedDeleteCommand(target, [unc(approvedRoot)], parseWslPath, false)
: null
expect(command).not.toBeNull()
try {
await expect(
execFileAsync('wsl.exe', ['-d', DISTRO, '--exec', ...(command ?? [])], {
encoding: 'utf-8',
timeout: 30000
})
).resolves.toMatchObject({ stderr: '' })
await expect(
wsl(
'test ! -e "$1/execute-only/vault/session.json" && test -f "$1/outside/unrelated"',
fixtureRoot
)
).resolves.toBe('')
} finally {
await wsl('chmod 700 "$1/execute-only"', fixtureRoot)
}
})
})
+9 -9
View File
@@ -14,12 +14,6 @@ missing_entry() {
done
return 0
}
inspect() {
inspected=$(stat -c '%d:%i:%F' -- "$1" 2>/dev/null) && return 0
missing_entry "$2" && exit 0
reject inspection
}
expected_kind=$1
root_count=$2
shift 2
@@ -29,10 +23,16 @@ while [ "$root_count" -gt 0 ]; do
component=$1
shift
root_count=$((root_count - 1))
inspect "$component" "$component"
inspected=$(stat -Lc '%d:%i:%F' -- "/proc/self/cwd/$component" 2>/dev/null) || {
missing_entry "$component" && exit 0
reject inspection
}
entry_kind=$(printf '%s' "$inspected" | cut -d: -f3-)
case "$entry_kind" in directory|'symbolic link') ;; *) reject inspection ;; esac
cd -P -- "$component" || reject inspection
[ "$entry_kind" = directory ] || reject inspection
expected_id=$(printf '%s' "$inspected" | cut -d: -f1-2)
cd -P -- "/proc/self/cwd/$component" || reject inspection
actual_id=$(stat -Lc '%d:%i' -- . 2>/dev/null) || reject inspection
[ "$actual_id" = "$expected_id" ] || reject race
done
root_physical=$(pwd -P) || reject inspection