From 86a88c87db2614cc4b9780f62b2ab896eacd0560 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 20 Oct 2026 16:06:41 -0700 Subject: [PATCH] fix(safe-paths): unlink planted symlinks as the customer, then fail closed Root rm -f in drop_symlink could be redirected by a parent-directory swap to delete arbitrary files as root. drop_symlink and the logs/ symlink sweep now unlink AS THE CUSTOMER (can never delete anything the customer couldn't), and drop_symlink still refuses to start if the path is a symlink afterwards; the pre-daemon logs/ gate re-checks and fails closed. Co-Authored-By: Claude Opus 5.5 --- scripts/safe-paths.sh | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/safe-paths.sh b/scripts/safe-paths.sh index ceab00e..f453d30 100644 --- a/scripts/safe-paths.sh +++ b/scripts/safe-paths.sh @@ -34,13 +34,14 @@ die() { exit 1 } -# drop_symlink PATH : if PATH is a symlink, unlink it (as root: rm on the final -# component never touches the link target, and the customer cannot deny it) and -# log it. Fails closed: if it is still a symlink afterwards, refuse to start. +# drop_symlink PATH : if PATH is a symlink, unlink it AS THE CUSTOMER (rm on +# the final component never touches the link target, and if a parent dir is +# swapped for a symlink mid-way the customer can only delete what they already +# could) and log it. Fails closed: still a symlink afterwards -> refuse to start. drop_symlink() { if [ -L "$1" ]; then echo "entrypoint: SECURITY: $1 was a symlink to '$(readlink "$1")' - removing" >&2 - rm -f -- "$1" + as_user rm -f -- "$1" [ -L "$1" ] && die "could not remove symlink $1" fi return 0 @@ -59,9 +60,11 @@ secure_home() { # public_html may legitimately be a symlink (release-dir deploys); it is left # alone and every root-side operation on it runs as the customer instead. if [ -d "$h/logs" ]; then - # find without -L never follows symlinks; -delete unlinks the link itself. - # Errors are not fatal HERE: require_safe_logs re-checks and fails closed. - find "$h/logs" -xdev -type l \ + # As the customer: find without -L never follows symlinks, -delete unlinks + # the link itself, and a raced parent swap can only reach what the customer + # could already delete. Errors are not fatal HERE: require_safe_logs + # re-checks immediately before the daemons start and fails closed. + as_user find "$h/logs" -xdev -type l \ -printf 'entrypoint: SECURITY: %p was a symlink to %l - removed\n' -delete >&2 || true fi }