From d400d5eb39cc7b28a0a7ee10919581f46af479bd Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 20 Oct 2026 16:04:21 -0700 Subject: [PATCH] fix(entrypoint): fail closed on planted symlinks before nginx starts drop_symlink unlinks as root (never follows the target) and refuses to start if the path is still a symlink. Immediately before nginx (which opens its logs under /home/$user/logs as root) starts, logs/, logs/nginx and logs/nodejs must be real directories and nothing under logs/ may be other than a regular file or directory, else the entrypoint exits 1. Co-Authored-By: Claude Opus 5.5 --- scripts/entrypoint.sh | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index c05cc5a..58e4317 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -31,26 +31,33 @@ as_user() { runuser -u "$user" -- "$@" fi } -# Unlink AS THE CUSTOMER, so a parent dir swapped for a symlink between the -# check and the rm can only reach what the customer could already. +# Fail closed: refusing to start only hurts the customer who planted the path, +# which is preferred to root following it. +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. drop_symlink() { if [ -L "$1" ]; then 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 + rm -f -- "$1" + [ -L "$1" ] && die "could not remove symlink $1" fi + return 0 } if [ -L /home/$user ] || [ ! -d /home/$user ]; then - echo "entrypoint: FATAL: /home/$user is not a real directory - refusing to start" >&2 - exit 1 + die "/home/$user is not a real directory" fi chown -h $user:$user /home/$user # app/ may legitimately be a symlink (releases/current deploys): it is left # alone, and every root-side operation on it below runs as the customer. drop_symlink /home/$user/logs -if [ -d /home/$user/logs ] && [ ! -L /home/$user/logs ]; then - # nginx opens its logs here as root. As the customer, find without -L never - # follows symlinks; -delete unlinks them. - as_user find /home/$user/logs -xdev -type l \ +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 \ -printf 'entrypoint: SECURITY: %p was a symlink to %l - removed\n' -delete >&2 || true fi @@ -81,6 +88,15 @@ chown -R $user:$user /home/$user chmod 755 /home/$user as_user find /home/$user -type d ! -perm -o=x -exec chmod o+x {} + 2>/dev/null || true +# nginx opens its logs under /home/$user/logs as root: last fail-closed check +# immediately before it starts. logs/, logs/nginx, logs/nodejs must be real +# dirs and nothing under logs/ may be other than a regular file or directory. +for d in /home/$user/logs /home/$user/logs/nginx /home/$user/logs/nodejs; do + [ -d "$d" ] && [ ! -L "$d" ] || die "$d is not a real directory" +done +bad=$(find /home/$user/logs -xdev ! -type f ! -type d -print -quit 2>&1) +[ -z "$bad" ] || die "unsafe entry under /home/$user/logs: $bad" + # Start nginx nginx