From 94a73ea8ab81dcbee8ab3c03945d5c027d545272 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 20 Oct 2026 15:52:01 -0700 Subject: [PATCH] fix(safe-paths): unlink symlinks and create files as the customer, never as root drop_symlink and the logs/ symlink sweep now unlink AS THE CUSTOMER, and user_touch no longer falls back to a root chown -h (a parent dir swapped for a symlink between check and use could still redirect a root path operation). A root-owned leftover error_log is fixed by the existing chown -R, which never follows symlinks. Every operation these helpers perform is now safe even if the customer races it. Co-Authored-By: Claude Opus 5.5 --- scripts/safe-paths.sh | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/scripts/safe-paths.sh b/scripts/safe-paths.sh index 21cd16e..8c17017 100644 --- a/scripts/safe-paths.sh +++ b/scripts/safe-paths.sh @@ -27,19 +27,20 @@ as_user() { fi } -# drop_symlink PATH : if PATH is a symlink, unlink it (rm on a symlink never -# touches the target) and log it. +# drop_symlink PATH : if PATH is a symlink, unlink it and log it. The unlink +# runs AS THE CUSTOMER: if a parent dir is swapped for a symlink between the +# check and the rm, the customer can only unlink what they could already. drop_symlink() { if [ -L "$1" ]; then - echo "entrypoint: SECURITY: $1 was a symlink to '$(readlink "$1")' - removed" >&2 - rm -f -- "$1" + echo "entrypoint: SECURITY: $1 was a symlink to '$(readlink "$1")' - removing" >&2 + as_user rm -f -- "$1" || echo "entrypoint: WARNING: could not remove symlink $1" >&2 fi } # secure_home : run once, right after adduser, before anything else touches # /home/$user. secure_home() { - local h="/home/$user" d + local h="/home/$user" if [ -L "$h" ] || [ ! -d "$h" ]; then echo "entrypoint: FATAL: $h is not a real directory (symlink or missing) - refusing to start" >&2 exit 1 @@ -49,11 +50,10 @@ secure_home() { drop_symlink "$h/logs" # 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 (start path verified real above). - find "$h/logs" -xdev -type l -print0 2>/dev/null | while IFS= read -r -d '' d; do - drop_symlink "$d" - done + if [ -d "$h/logs" ] && [ ! -L "$h/logs" ]; then + # As the customer, find without -L never follows symlinks; -delete unlinks. + as_user find "$h/logs" -xdev -type l \ + -printf 'entrypoint: SECURITY: %p was a symlink to %l - removed\n' -delete >&2 || true fi } @@ -62,12 +62,13 @@ user_mkdir() { as_user mkdir -p -- "$@" } -# user_touch FILE : create FILE as the customer. If it already exists owned by -# someone else (root-created by an older image), fix ownership with chown -h, -# which never follows a symlink; a symlink there is dropped first. +# user_touch FILE : create FILE as the customer (a symlink there is dropped +# first). Root never touches/chowns it: if it already exists root-owned (left by +# an older image) the touch fails quietly and the entrypoint's chown -R, which +# never follows symlinks, fixes the owner. user_touch() { drop_symlink "$1" - as_user touch -- "$1" 2>/dev/null || chown -h "$user:$user" -- "$1" 2>/dev/null || true + as_user touch -- "$1" 2>/dev/null || true } # load_user_crontab : install /home/$user/crontab, read AS THE CUSTOMER. Root