diff --git a/lib/build.func b/lib/build.func index 6f02239..f54ebc5 100644 --- a/lib/build.func +++ b/lib/build.func @@ -116,20 +116,25 @@ sanitize_input() { # bytes). Byte-exact via tr: delete all printable ASCII — anything left # over is junk. is_clean_ascii() { + # Newline/Tab zuerst explizit ablehnen — $(…) strippt trailing newlines, + # die der tr-Pfad sonst übersehen würde (Review-Finding K-114). + [[ "$1" == *$'\n'* || "$1" == *$'\t'* ]] && return 1 local leftover leftover="$(printf '%s' "$1" | LC_ALL=C tr -d '\40-\176')" [[ -z "$leftover" ]] } is_uint() { is_clean_ascii "$1" && [[ "$1" =~ ^[0-9]+$ ]]; } -is_vlan_tag() { is_uint "$1" && (( $1 >= 1 && $1 <= 4094 )); } +# 10#: führende Nullen nicht als Oktal werten ("08" wäre sonst ein +# Arithmetik-Fehler statt einer sauberen Ablehnung/Annahme). +is_vlan_tag() { is_uint "$1" && (( 10#$1 >= 1 && 10#$1 <= 4094 )); } is_token() { is_clean_ascii "$1" && [[ "$1" =~ ^[A-Za-z0-9._-]+$ ]]; } is_hostname() { is_clean_ascii "$1" && [[ "$1" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]]; } is_ipv4() { is_clean_ascii "$1" && [[ "$1" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]] || return 1 local o - for o in "${BASH_REMATCH[@]:1:4}"; do (( o <= 255 )) || return 1; done + for o in "${BASH_REMATCH[@]:1:4}"; do (( 10#$o <= 255 )) || return 1; done return 0 } @@ -137,13 +142,15 @@ is_cidr() { [[ "$1" =~ ^([0-9.]+)/([0-9]{1,2})$ ]] || return 1 # BASH_REMATCH retten — is_ipv4 nutzt selbst =~ und überschreibt es. local _ip="${BASH_REMATCH[1]}" _prefix="${BASH_REMATCH[2]}" - is_ipv4 "$_ip" && (( _prefix >= 1 && _prefix <= 32 )) + is_ipv4 "$_ip" && (( 10#$_prefix >= 1 && 10#$_prefix <= 32 )) } is_ipcfg() { [[ "$1" == "dhcp" ]] || is_cidr "$1"; } -# Space/comma-separated list of IPv4s (DNS prompt). +# Space/comma-separated list of IPv4s (DNS prompt). Gesamtstring zuerst +# prüfen — die Wort-Splittung würde eingebettete Newlines sonst verstecken. is_ipv4_list() { + is_clean_ascii "$1" || return 1 local item for item in ${1//,/ }; do is_ipv4 "$item" || return 1; done [[ -n "$1" ]] @@ -154,11 +161,18 @@ is_ipv4_list() { # empty input and re-prompts until the validator passes. allow_empty=yes lets # an empty value through (e.g. "no VLAN"). prompt_validated() { + # Schutz vor zirkulärem nameref (Review-Finding): interne Namen tabu. + [[ "$1" == _pv_* || "$1" == _rv_* ]] && { msg_err "prompt_validated: reserved variable name '$1'"; return 2; } local -n _pv_ref="$1" local _pv_prompt="$2" _pv_validator="$3" _pv_default="${4-}" _pv_allow_empty="${5:-no}" local _pv_value while true; do - read -rp "$_pv_prompt" _pv_value + if ! read -rp "$_pv_prompt" _pv_value; then + # EOF (kein TTY / stdin erschöpft): kein Endlos-Loop, sauber raus — + # unter set -e bricht der Caller damit kontrolliert ab. + msg_err "No input available for prompt: ${_pv_prompt%% *}" + return 1 + fi _pv_value="$(sanitize_input "$_pv_value")" if [[ -z "$_pv_value" && -n "$_pv_default" ]]; then _pv_value="$_pv_default" @@ -182,6 +196,7 @@ prompt_validated() { # Validate an env-provided value (non-interactive: abort instead of re-prompt). require_valid() { + [[ "$1" == _pv_* || "$1" == _rv_* ]] && { msg_err "require_valid: reserved variable name '$1'"; return 2; } local -n _rv_ref="$1" local _rv_validator="$2" _rv_label="$3" _rv_ref="$(sanitize_input "$_rv_ref")" @@ -195,11 +210,24 @@ require_valid() { # via env skips the prompt (= "no VLAN" / "inherit DNS from host"). prompt_lxc_config() { if [[ -z "${CTID:-}" ]]; then - read -rp "Container ID [auto]: " CTID - CTID="$(sanitize_input "$CTID")" - [[ -z "${CTID:-}" ]] && CTID=$(pvesh get /cluster/nextid) + # Eigener Loop statt prompt_validated: leer = auto (pvesh nextid), + # ungültig = Re-Prompt (Review-Finding: vorher Abbruch statt Re-Prompt). + while true; do + if ! read -rp "Container ID [auto]: " CTID; then + msg_err "No input available for prompt: Container ID" + return 1 + fi + CTID="$(sanitize_input "$CTID")" + if [[ -z "$CTID" ]]; then + CTID=$(pvesh get /cluster/nextid) + break + fi + is_uint "$CTID" && break + msg_warn "Invalid value: '$CTID' — please retry (digits only)." + done + else + require_valid CTID is_uint "Container ID" fi - require_valid CTID is_uint "Container ID" echo " → CTID: $CTID" if [[ -z "${CT_HOSTNAME:-}" ]]; then @@ -266,7 +294,12 @@ prompt_lxc_config() { # 4. DHCP, no profile → inherit from host apply_network_profile if [[ -n "${NAMESERVER+x}" ]]; then - : # explicit override from env, leave untouched + # Explizites Override aus env: "" = inherit bleibt erlaubt, aber ein + # gesetzter Wert wird validiert (Review-Finding: lief vorher ungeprüft + # bis in pct create). + if [[ -n "${NAMESERVER:-}" ]]; then + require_valid NAMESERVER is_ipv4_list "DNS server" + fi elif [[ -n "$PROFILE_DNS" ]]; then NAMESERVER="$PROFILE_DNS" msg_info "DNS for VLAN ${VLAN_TAG:-none} (${PROFILE_SUBNET:-?}): $NAMESERVER" diff --git a/tests/check_ct_source.sh b/tests/check_ct_source.sh index 7fe565f..a0293fe 100755 --- a/tests/check_ct_source.sh +++ b/tests/check_ct_source.sh @@ -3,6 +3,10 @@ # gesourct, Skript stirbt erst mitten im Lauf" ist zweimal real passiert # (nexus-db: im Review gefangen; authentik: erst in Produktion). # Aufruf: tests/check_ct_source.sh [verzeichnis] (Default: ct/) +# +# Bewusst eng: akzeptiert wird NUR die curl-Prozesssubstitutions-Form +# `source <(curl ... build.func)` — das ist der Repo-Vertrag für ct/-Scripts +# (Review-Triage K-114). Lokales Sourcen gehört nicht in ct/*.sh. set -euo pipefail DIR="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/ct}" diff --git a/tests/test_validation.sh b/tests/test_validation.sh index 57f0e67..08838fe 100755 --- a/tests/test_validation.sh +++ b/tests/test_validation.sh @@ -108,6 +108,39 @@ else nok "alle ct/*.sh sourcen build.func" fi +# ── Review-Findings K-114: Oktal, Newline, EOF, Nameref-Guard ──────────────── +assert_true "is_vlan_tag akzeptiert '08' (kein Oktal-Fehler)" is_vlan_tag "08" +assert_true "is_cidr akzeptiert /08 (kein Oktal-Fehler)" is_cidr "10.0.0.1/08" +assert_false "is_clean_ascii lehnt eingebettetes Newline ab" is_clean_ascii $'1.1.1.1\n8.8.8.8' +assert_false "is_ipv4_list lehnt Newline-Liste ab" is_ipv4_list $'1.1.1.1\n8.8.8.8' + +# EOF statt Eingabe: prompt_validated darf nicht endlos loopen. +( printf '' | { V=""; prompt_validated V "Wert: " is_uint; } ) >/dev/null 2>&1 +rc=$? +[[ "$rc" -ne 0 ]] && ok "prompt_validated bricht bei EOF ab (rc=$rc)" \ + || nok "prompt_validated bricht bei EOF ab" + +# Reservierte Namen → Guard statt zirkulärem nameref. +( _pv_ref=""; prompt_validated _pv_ref "x: " is_uint ) >/dev/null 2>&1 +[[ $? -eq 2 ]] && ok "prompt_validated weist reservierte Variablennamen ab" \ + || nok "prompt_validated weist reservierte Variablennamen ab" + +# CTID-Re-Prompt: ungültig → erneut fragen, leer wäre auto (hier: gültige Zahl). +ctid_out="$( + printf 'abc\n123\n' | env -u CTID bash -c " + source '$REPO_ROOT/lib/build.func' + CTID='' + while true; do + read -rp 'Container ID [auto]: ' CTID || exit 1 + CTID=\"\$(sanitize_input \"\$CTID\")\" + [[ -z \"\$CTID\" ]] && exit 1 + is_uint \"\$CTID\" && break + done + printf '%s' \"\$CTID\"" +)" +[[ "$ctid_out" == "123" ]] && ok "CTID-Loop re-promptet bei 'abc' und nimmt '123'" \ + || nok "CTID-Loop re-promptet (got: '$ctid_out')" + # ── Dry-Run: prompt_lxc_config komplett aus env, ohne PVE/TTY ──────────────── smoke_out="$( env CTID=999 CT_HOSTNAME=smoke DISK_SIZE=8 CORES=2 RAM=1024 BRIDGE=vmbr0 \