mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
fix(ai-vault): close approved-root traversal race
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user