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 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-20 15:52:01 -07:00
1 parent 0ffccac4b8
commit 94a73ea8ab
1 file changed
+15 -14
+15 -14
View File
@@ -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