From 71c1999e27be3b8557fda9b264c73c2c8ba387a3 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 23 Oct 2026 22:46:24 +1100 Subject: [PATCH] Abort on invalid allowlist lines; refuse launch dirs exposing $HOME or the allowlist --- README.md | 2 ++ agent-sandbox | 26 ++++++++++++++++++++++++-- tests/test.sh | 28 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc7c433..8f29076 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ agent-sandbox --exec bash # a shell in the sandbox instead of claude - `$PWD` is mounted read-write at the same path. Files you create there are owned by your host user. Nothing else from the host is mounted, and SELinux labels on your files are never changed (`label=disable`). +- It refuses to launch from `$HOME`, any parent of it, or any directory + containing the allowlist. - Claude's home is the `agent-sandbox-home` podman volume (login, config and nested container images live here). All sessions share it. - `podman` works inside the sandbox (nested rootless). The host podman socket is diff --git a/agent-sandbox b/agent-sandbox index 00d1149..3a12b5b 100755 --- a/agent-sandbox +++ b/agent-sandbox @@ -61,7 +61,9 @@ router_config() { local -A ip=() local hosts tcp="" forwards="0.0.0.0:443:$rip:443 0.0.0.0:80:$rip:80" - hosts=$(allowlist_entries "$allowlist" | awk '{print $1}' | uniq) + local entries + entries=$(allowlist_entries "$allowlist") || return + hosts=$(awk '{print $1}' <<<"$entries" | uniq) for host in $hosts; do [[ $host == .* ]] && continue ip[$host]=127.1.$((i / 250)).$((i % 250 + 1)) @@ -72,7 +74,7 @@ router_config() { tcp+=" server { listen $rip:$((PORT_BASE + j)); set \$up $host:$port; proxy_pass \$up; }"$'\n' forwards+=" ${ip[$host]}:$port:$rip:$((PORT_BASE + j))" j=$((j + 1)) - done < <(allowlist_entries "$allowlist") + done <<<"$entries" { printf '%s\n' no-resolv no-hosts bind-interfaces "listen-address=$rip" "address=/#/" @@ -137,6 +139,24 @@ EOF echo "$forwards" } +# True if path $1 is $2 or lies under it. +inside() { + [[ $1 == "$2" || $1 == "${2%/}"/* ]] +} + +# Refuse launch dirs that would mount $HOME or the allowlist read-write. +check_launch_dir() { + local pwd home dir + pwd=$(realpath "$PWD") + home=$(realpath -m "$HOME") + inside "$home" "$pwd" && die "refusing to mount $pwd: it is or contains \$HOME" + for dir in "$CONFIG_DIR" "$(dirname "$ALLOWLIST")"; do + dir=$(realpath -m "$dir") + inside "$dir" "$pwd" && die "refusing to mount $pwd: it contains $dir" + done + return 0 +} + main() { local cmd=(claude "$@") if [[ ${1:-} == --exec ]]; then @@ -144,6 +164,7 @@ main() { cmd=("$@") fi [[ $PWD == *:* ]] && die "cannot mount a directory containing ':'" + check_launch_dir if [[ ! -f $ALLOWLIST ]]; then local default @@ -154,6 +175,7 @@ main() { cp "$default" "$ALLOWLIST" echo "agent-sandbox: created $ALLOWLIST from defaults" >&2 fi + allowlist_entries "$ALLOWLIST" >/dev/null || exit 1 local session router tmp subnet rip forwards session=agent-sandbox-$(date +%s)-$$ diff --git a/tests/test.sh b/tests/test.sh index c203474..4480537 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -70,4 +70,32 @@ done check default-allowlist-parses 0 "$( (allowlist_entries allowed-hosts >/dev/null) 2>&1; echo $?)" +f=$(list 'ok.example.com' 'bad host' 'z.example.com') +check router-config-fails-on-bad-line 1 "$( (router_config "$f" "$(mktemp -d)" 10.89.7.2) >/dev/null 2>&1; echo $?)" + +# Launch through main with a stub podman that logs its calls. +work=$(mktemp -d) +mkdir -p "$work/bin" "$work/home/proj" "$work/other" +printf '%s\n' '#!/bin/sh' "echo \"\$*\" >>$work/podman.log" \ + 'case "$*" in "network inspect"*) echo 10.89.7.0/24 ;; esac' >"$work/bin/podman" +chmod +x "$work/bin/podman" +launch() { + local dir=$1 + shift + rm -f "$work/podman.log" + (cd "$dir" && env PATH="$work/bin:$PATH" HOME="$work/home" XDG_RUNTIME_DIR="$work" "$@" \ + "$OLDPWD/agent-sandbox" --exec true) >/dev/null 2>&1 + echo "$? $([[ -s $work/podman.log ]] && echo podman || echo none)" +} +good=$(list 'api.anthropic.com') +check launch-from-home-subdir "0 podman" "$(launch "$work/home/proj" AGENT_SANDBOX_ALLOWLIST="$good")" +check launch-refuses-bad-line "1 none" "$(launch "$work/home/proj" AGENT_SANDBOX_ALLOWLIST="$(list 'ok.example.com' 'bad host')")" +check launch-refuses-home "1 none" "$(launch "$work/home" AGENT_SANDBOX_ALLOWLIST="$good")" +check launch-refuses-home-ancestor "1 none" "$(launch "$work" AGENT_SANDBOX_ALLOWLIST="$good")" +check launch-refuses-root "1 none" "$(launch / AGENT_SANDBOX_ALLOWLIST="$good")" +ln -s "$work/home" "$work/homelink" +check launch-refuses-home-symlink "1 none" "$(launch "$work/homelink" AGENT_SANDBOX_ALLOWLIST="$good")" +check launch-refuses-config-parent "1 none" "$(launch "$work/other" XDG_CONFIG_HOME="$work/other/cfg" AGENT_SANDBOX_ALLOWLIST="$good")" +check launch-refuses-allowlist-dir "1 none" "$(launch "$work/home/proj" AGENT_SANDBOX_ALLOWLIST="$work/home/proj/hosts")" + ((fails == 0)) || { echo "$fails failed"; exit 1; }