From 4064d9d6aa2e5998eb6e1ef6bfe8a32f5206957e Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 20 Oct 2026 16:06:41 -0700 Subject: [PATCH] fix(entrypoint): 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/entrypoint.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 58e4317..7e70b2d 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -37,12 +37,14 @@ die() { echo "entrypoint: FATAL: $* - refusing to start" >&2 exit 1 } -# Unlink as root: rm on the final component never touches the link target and -# the customer cannot deny it. Still a symlink afterwards -> refuse to start. +# Unlink 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. Still a symlink afterwards -> refuse to +# start (fail closed). 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 @@ -55,9 +57,11 @@ chown -h $user:$user /home/$user # alone, and every root-side operation on it below runs as the customer. drop_symlink /home/$user/logs if [ -d /home/$user/logs ]; then - # nginx opens its logs here as root. find without -L never follows symlinks; - # -delete unlinks the link itself. Re-checked fail-closed before nginx starts. - find /home/$user/logs -xdev -type l \ + # nginx opens its logs here as root. 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. Re-checked + # fail-closed immediately before nginx starts. + as_user find /home/$user/logs -xdev -type l \ -printf 'entrypoint: SECURITY: %p was a symlink to %l - removed\n' -delete >&2 || true fi