From 5967b7b7e55316ebfddcd4b8a6ba6a64152fa864 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 20 Jun 2026 13:50:19 +0200 Subject: [PATCH] fix: shell-escape the install dir when generating the clusev CLI (R15 Codex) A checkout path containing shell syntax ($(), quotes) could otherwise inject code into /usr/local/bin/clusev, which runs as root via 'sudo clusev update'. Render the path through printf %q into an unquoted assignment; add a test that a hostile path does not execute injected code. --- docker/clusev/clusev | 4 +++- install.sh | 11 +++++++---- tests/scripts/test-clusev-cli.sh | 12 ++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docker/clusev/clusev b/docker/clusev/clusev index 4079bec..9f2658f 100644 --- a/docker/clusev/clusev +++ b/docker/clusev/clusev @@ -6,7 +6,9 @@ # referenced internally only — it is never printed to the operator. set -euo pipefail -CLUSEV_DIR="__CLUSEV_DIR__" +# __CLUSEV_DIR__ is replaced by install.sh with a printf %q shell-escaped path, so this +# unquoted assignment stays injection-safe even if the install path contains shell syntax. +CLUSEV_DIR=__CLUSEV_DIR__ COMPOSE_FILE="${CLUSEV_DIR}/docker-compose.prod.yml" compose() { docker compose -f "$COMPOSE_FILE" "$@"; } diff --git a/install.sh b/install.sh index c47a158..7888851 100755 --- a/install.sh +++ b/install.sh @@ -315,11 +315,14 @@ ADMIN_MAIL="$(printf '%s\n' "$INSTALL_OUT" | sed -n 's/^CLUSEV_ADMIN_EMAIL=//p' # ── [9/9] Host-CLI + MOTD ──────────────────────────────────────────── phase 9/9 "Host-CLI + MOTD installieren" -# clusev host command: bake the install dir into the template, drop it in PATH. Literal -# (not sed) substitution so any path is safe; best-effort with the whole group's stderr muted -# so a read-only /usr/local/bin never fails the installer (no stray "Permission denied"). +# clusev host command: bake the install dir into the template, drop it in PATH. The path is +# printf %q shell-escaped before substitution so a checkout path containing shell syntax cannot +# inject code into the generated CLI (which later runs as root via `sudo clusev update`). Literal +# (not sed) substitution; best-effort with the whole group's stderr muted so a read-only +# /usr/local/bin never fails the installer (no stray "Permission denied"). cli_template="$(cat docker/clusev/clusev)" -cli_rendered="${cli_template//__CLUSEV_DIR__/$(pwd)}" +cli_dir="$(printf '%q' "$(pwd)")" +cli_rendered="${cli_template//__CLUSEV_DIR__/$cli_dir}" if { printf '%s\n' "$cli_rendered" > /usr/local/bin/clusev && chmod 0755 /usr/local/bin/clusev; } 2>/dev/null; then info "Host-Befehl installiert: clusev (z. B. 'clusev ps', 'sudo clusev update')" else diff --git a/tests/scripts/test-clusev-cli.sh b/tests/scripts/test-clusev-cli.sh index b6797fa..204b842 100644 --- a/tests/scripts/test-clusev-cli.sh +++ b/tests/scripts/test-clusev-cli.sh @@ -46,4 +46,16 @@ grep -qF "/usr/local/bin/clusev" install.sh || fail "install.sh does not install # 7. usage text must NOT leak the long compose filename. printf '%s' "$out" | grep -qF "docker-compose.prod.yml" && fail "usage leaks docker-compose.prod.yml" +# 8. A hostile install path (shell syntax) must not inject code into the rendered CLI. +# install.sh escapes the path with printf %q before substituting it; mimic that here and +# confirm running the rendered script does NOT execute the embedded command. +evil="/tmp/x \$(touch ${TMP}/PWNED) y" +esc="$(printf '%q' "$evil")" +tmpl="$(cat docker/clusev/clusev)" +printf '%s' "${tmpl//__CLUSEV_DIR__/$esc}" > "${TMP}/clusev-evil" +chmod +x "${TMP}/clusev-evil" +bash -n "${TMP}/clusev-evil" || fail "rendered evil-path CLI has bad syntax" +"${TMP}/clusev-evil" help >/dev/null 2>&1 || true +[ ! -e "${TMP}/PWNED" ] || fail "hostile install path injected code into the rendered CLI" + printf 'ok — clusev CLI checks passed\n'