From b3d713b6bb29d195e16ae8f43d6835b4e512f20b Mon Sep 17 00:00:00 2001 From: Ismael Leon Date: Wed, 14 Oct 2026 19:05:54 -0600 Subject: [PATCH 1/2] Never let root run a file the bot can write The yt-dlp updater ran as root straight from the checkout, which the bot's unit could write. Install a root-owned copy in /usr/local/sbin, sandbox the updater, and narrow the bot's writable paths to its caches and cookies. --- deploy/install-units.sh | 31 ++++++++++++++++++++++++++++--- deploy/update-ytdlp.sh | 7 +++++-- tests/test_service_sandbox.py | 23 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/deploy/install-units.sh b/deploy/install-units.sh index 8533305..1c0d549 100755 --- a/deploy/install-units.sh +++ b/deploy/install-units.sh @@ -16,6 +16,7 @@ APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" VENV_DIR="$APP_DIR/.venv" SERVICE_NAME="loopify-bot" UPDATER_NAME="loopify-ytdlp-update" +UPDATER_BIN="/usr/local/sbin/$UPDATER_NAME" # Whoever owns the checkout is who the bot runs as. APP_USER="$(stat -c '%U' "$APP_DIR")" @@ -78,7 +79,10 @@ NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=read-only -ReadWritePaths=$APP_DIR +# Only the caches, and the cookies yt-dlp writes back. Never the code: the bot +# parses untrusted media, and a bot that can rewrite main.py keeps whatever got +# in across restarts. +ReadWritePaths=$APP_DIR/.cache -$APP_DIR/cookies.txt ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true @@ -97,9 +101,15 @@ TasksMax=256 WantedBy=multi-user.target UNIT +mkdir -p "$APP_DIR/.cache" + # yt-dlp is the one dependency that must NOT be pinned: YouTube changes its # player constantly and a stale build stops resolving videos within weeks. -chmod +x "$APP_DIR/deploy/update-ytdlp.sh" +# +# The updater runs as root, so root runs a copy root owns. Run from the +# checkout, it was a file $APP_USER can write, and so one step from anything +# that ever runs as $APP_USER. +sudo install -o root -g root -m 0755 "$APP_DIR/deploy/update-ytdlp.sh" "$UPDATER_BIN" sudo tee "/etc/systemd/system/${UPDATER_NAME}.service" >/dev/null </dev/null </dev/null < Date: Wed, 14 Oct 2026 19:07:21 -0600 Subject: [PATCH 2/2] Keep .cache owned by the app user when the units are installed as root --- deploy/install-units.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/install-units.sh b/deploy/install-units.sh index 1c0d549..ac0d2f4 100755 --- a/deploy/install-units.sh +++ b/deploy/install-units.sh @@ -101,7 +101,8 @@ TasksMax=256 WantedBy=multi-user.target UNIT -mkdir -p "$APP_DIR/.cache" +# Owned by $APP_USER even when this runs as root: the bot writes nowhere else. +sudo install -d -o "$APP_USER" -g "$APP_USER" -m 0755 "$APP_DIR/.cache" # yt-dlp is the one dependency that must NOT be pinned: YouTube changes its # player constantly and a stale build stops resolving videos within weeks.