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.
feat/v1-foundation
boban 2026-06-20 13:50:19 +02:00
parent 8d2b7087a9
commit 5967b7b7e5
3 changed files with 22 additions and 5 deletions

View File

@ -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" "$@"; }

View File

@ -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

View File

@ -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'