feat: safe config parser without source (2.3.0-SAC)

Whitelist-based /etc/ssh-monitor.conf load; validate URLs/enums/CIDR; remove BACKUP_WEBHOOK_URL.
This commit is contained in:
PTah
2026-07-08 12:57:36 +10:00
parent 6511d34ca7
commit 18a65078fc
11 changed files with 308 additions and 114 deletions
+207
View File
@@ -23,6 +23,213 @@ ssh_monitor_config_peek() {
printf '%s\n' "$val"
}
# --- Безопасная загрузка /etc/ssh-monitor.conf (2.3.0+: без source / выполнения bash) ---
_ssh_monitor_config_trim() {
local v="$1"
v="${v#"${v%%[![:space:]]*}"}"
v="${v%"${v##*[![:space:]]}"}"
printf '%s' "$v"
}
ssh_monitor_config_is_known_key() {
case "$1" in
TELEGRAM_BOT_TOKEN | TELEGRAM_CHAT_ID | NOTIFY_ORDER | \
UseSAC | SAC_URL | SAC_API_KEY | SAC_SPOOL_DIR | SAC_FAIL_COUNT_FILE | \
SAC_FALLBACK_FAILURES | SAC_TIMEOUT_SEC | SAC_SPOOL_FLUSH_MAX_FILES | \
SAC_SPOOL_MAX_AGE_HOURS | SAC_TLS_INSECURE | SAC_SEND_HEARTBEAT | SAC_AGENT_ID_FILE | \
CONFIG_STRICT_PERMS | \
MAIL_SMTP_HOST | MAIL_SMTP_PORT | MAIL_SMTP_USER | MAIL_SMTP_PASSWORD | \
MAIL_FROM | MAIL_TO | MAIL_SMTP_STARTTLS | MAIL_SMTP_SSL | \
LOG_FILE | LAST_HEARTBEAT_FILE | HEARTBEAT_INTERVAL_MINUTES | LAST_REPORT_FILE | \
LAST_SSH_CHECK_FILE | LAST_SUDO_CHECK_FILE | LAST_SECURITY_EVENTS_FILE | LAST_LOGIND_CHECK_FILE | \
DAILY_REPORT_ENABLED | DAILY_REPORT_HOUR | DAILY_REPORT_TZ | DAILY_REPORT_TOP_IPS | \
NOTIFY_TZ | SERVER_DISPLAY_NAME | SERVER_IPV4 | SSH_ACCEPT_NOTIFY_DEDUP_SEC | \
ENABLE_LOGIND_MONITOR | LOGIND_NOTIFY_NEW | LOGIND_NOTIFY_REMOVED | LOGIND_NOTIFY_FAILED | LOGIND_SKIP_REMOTE | \
PROMETHEUS_TEXTFILE_DIR | HEALTHCHECK_STATUS_FILE | \
BRUTE_WINDOW_SEC | BRUTE_MIN_FAILS | BRUTE_NOTIFY_COOLDOWN_SEC | \
ENABLE_IP_BAN | BAN_TIME | MAX_ATTEMPTS | BAN_CHECK_INTERVAL | MONITOR_INTERVAL | BAN_LIST_FILE | \
IPSET_NAME_V4 | IPSET_NAME_V6 | IPSET_MAXELEM | \
WHITELIST_IPS | WHITELIST_SUBNETS | \
WATCHDOG_MAX_HEARTBEAT_AGE | WATCHDOG_LOG_FILE | WATCHDOG_SERVICE_NAME | WATCHDOG_NOTIFY_ON_RECOVERY | \
GIT_REF | GIT_VERIFY_MODE | GIT_ALLOW_RESET | GIT_BRANCH | REPO_URL)
return 0
;;
BACKUP_WEBHOOK_URL)
return 2
;;
esac
return 1
}
# Разбор строки KEY=value. Устанавливает _CFG_KEY / _CFG_VAL.
# 0 ok | 1 пусто/комментарий | 2 битая строка | 3 подстановка shell в value
ssh_monitor_config_parse_line() {
local line="$1" raw_val
_CFG_KEY="" _CFG_VAL=""
line="$(_ssh_monitor_config_trim "$line")"
[ -n "$line" ] || return 1
[[ "$line" == \#* ]] && return 1
if [[ ! "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
return 2
fi
_CFG_KEY="${BASH_REMATCH[1]}"
raw_val="${BASH_REMATCH[2]}"
raw_val="$(_ssh_monitor_config_trim "$raw_val")"
if [ -z "$raw_val" ]; then
_CFG_VAL=""
return 0
fi
case "$raw_val" in
\"*\")
_CFG_VAL="${raw_val#\"}"
_CFG_VAL="${_CFG_VAL%\"}"
;;
\'*\')
_CFG_VAL="${raw_val#\'}"
_CFG_VAL="${_CFG_VAL%\'}"
;;
*)
_CFG_VAL="$raw_val"
;;
esac
if [[ "$_CFG_VAL" == *'$('* || "$_CFG_VAL" == *'`'* ]]; then
return 3
fi
return 0
}
ssh_monitor_config_load_file() {
local file="$1"
local line rc line_no=0
[ -f "$file" ] || return 1
while IFS= read -r line || [ -n "$line" ]; do
line_no=$((line_no + 1))
ssh_monitor_config_parse_line "$line"
rc=$?
if [ "$rc" -eq 1 ]; then
continue
fi
if [ "$rc" -eq 2 ]; then
printf 'WARN: конфиг %s:%s: пропуск нераспознанной строки: %s\n' "$file" "$line_no" "$line" >&2
continue
fi
if [ "$rc" -eq 3 ]; then
printf 'ERROR: конфиг %s:%s: недопустимая подстановка shell в %s=\n' "$file" "$line_no" "$_CFG_KEY" >&2
return 1
fi
ssh_monitor_config_is_known_key "$_CFG_KEY"
rc=$?
if [ "$rc" -eq 2 ]; then
printf 'WARN: конфиг %s:%s: BACKUP_WEBHOOK_URL удалён в 2.3.0-SAC — игнорируется\n' "$file" "$line_no" >&2
continue
fi
if [ "$rc" -eq 1 ]; then
printf 'WARN: конфиг %s:%s: неизвестный ключ %s (игнорируется)\n' "$file" "$line_no" "$_CFG_KEY" >&2
continue
fi
printf -v "$_CFG_KEY" '%s' "$_CFG_VAL"
done <"$file"
return 0
}
ssh_monitor_validate_ipv4() {
local ip="$1" o
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
for o in ${ip//./ }; do
[ "$o" -le 255 ] || return 1
done
return 0
}
ssh_monitor_validate_cidr_v4() {
local cidr="$1" net mask
[[ "$cidr" == */* ]] || return 1
net="${cidr%/*}"
mask="${cidr#*/}"
[[ "$mask" =~ ^[0-9]+$ ]] && [ "$mask" -ge 0 ] && [ "$mask" -le 32 ] || return 1
ssh_monitor_validate_ipv4 "$net"
}
ssh_monitor_validate_http_url() {
local name="$1" value="$2" optional="${3:-0}"
value="$(_ssh_monitor_config_trim "$value")"
if [ -z "$value" ]; then
[ "$optional" = "1" ] && return 0
printf 'ERROR: %s: пустой URL\n' "$name" >&2
return 1
fi
if [[ ! "$value" =~ ^https?://[^/[:space:]]+ ]]; then
printf 'ERROR: %s: ожидается http(s)://host…, получено %s\n' "$name" "$value" >&2
return 1
fi
return 0
}
ssh_monitor_validate_hostname() {
local name="$1" value="$2"
value="$(_ssh_monitor_config_trim "$value")"
[ -z "$value" ] && return 0
if [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$ ]]; then
printf 'ERROR: %s: недопустимый hostname %s\n' "$name" "$value" >&2
return 1
fi
return 0
}
# Проверка значений после load_config (exit 1 при ERROR).
ssh_monitor_validate_config_values() {
local err=0 mode item
if [ -n "${SAC_URL:-}" ]; then
ssh_monitor_validate_http_url "SAC_URL" "${SAC_URL}" 1 || err=1
fi
if [ -n "${MAIL_SMTP_HOST:-}" ]; then
ssh_monitor_validate_hostname "MAIL_SMTP_HOST" "${MAIL_SMTP_HOST}" || err=1
fi
if [ -n "${SERVER_IPV4:-}" ]; then
ssh_monitor_validate_ipv4 "${SERVER_IPV4}" || {
printf 'ERROR: SERVER_IPV4: некорректный IPv4 %s\n' "${SERVER_IPV4}" >&2
err=1
}
fi
mode="$(printf '%s' "${UseSAC:-off}" | tr '[:upper:]' '[:lower:]')"
case "$mode" in
off | exclusive | dual | fallback) ;;
*)
printf 'ERROR: UseSAC="%s" (допустимо: off, exclusive, dual, fallback)\n' "${UseSAC:-}" >&2
err=1
;;
esac
mode="$(printf '%s' "${GIT_VERIFY_MODE:-off}" | tr '[:upper:]' '[:lower:]')"
case "$mode" in
off | tag | commit) ;;
*)
printf 'ERROR: GIT_VERIFY_MODE="%s" (допустимо: off, tag, commit)\n' "${GIT_VERIFY_MODE:-}" >&2
err=1
;;
esac
for item in "${WHITELIST_IPS[@]:-}"; do
[ -z "$item" ] && continue
if ! ssh_monitor_validate_ipv4 "$item"; then
printf 'ERROR: WHITELIST_IPS: некорректный IPv4 %s\n' "$item" >&2
err=1
fi
done
for item in "${WHITELIST_SUBNETS[@]:-}"; do
[ -z "$item" ] && continue
if ! ssh_monitor_validate_cidr_v4 "$item"; then
printf 'ERROR: WHITELIST_SUBNETS: некорректный CIDR %s\n' "$item" >&2
err=1
fi
done
return "$err"
}
ssh_monitor_truthy() {
case "${1,,}" in
1 | yes | true | on) return 0 ;;