From b5937cc39bcbecbe3d676b2b82b8fb377310290b Mon Sep 17 00:00:00 2001 From: robott Date: Tue, 15 Sep 2026 17:19:50 +0200 Subject: [PATCH] update mail-status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kritické: command execution cez env premennú Kontrola limitov bola v zlom poradí: bash (( MAIL_STATS_CPU_SEC > 0 && MAIL_STATS_MEM_KB > 0 && MAIL_STATS_FSIZE_KB > 0 )) || die ... need_uint MAIL_STATS_FSIZE_KB "$MAIL_STATS_FSIZE_KB" # validácia až PO arithmetic Bash v (( )) vyhodnocuje obsah premennej ako výraz, takže subscript sa vykoná ako príkaz: bash MAIL_STATS_FSIZE_KB='UID[$(touch /tmp/PWNED)]' ./mail-stats ls /tmp/PWNED # existuje, vlastník root --- mail-status.sh | 103 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/mail-status.sh b/mail-status.sh index 71b2e62..58c861e 100644 --- a/mail-status.sh +++ b/mail-status.sh @@ -1,11 +1,19 @@ #!/bin/bash # ============================================================================== -# mail-stats - Postfix Mail Summary Tool (v5.0.1) +# mail-stats - Postfix Mail Summary Tool (v5.0.2) # # Requires: bash >= 4.2, GNU coreutils/findutils, util-linux (flock), # gawk (recommended) or mawk 1.3.4 (verified by a self-test at start) # Optional: gzip/xz/bzip2/zstd (rotated logs), journalctl, less # +# 5.0.2: security fixes +# - arithmetic injection via MAIL_STATS_* (command execution) fixed +# - env hardening (TMPDIR/XDG_RUNTIME_DIR ignored as root, LESS*/GZIP/XZ/ +# ZSTD/AWK env cleared), root lock in private /run/mail-stats (0700) +# - less runs with a clean environment (no history file, secure mode) +# - filter length limit, regex validation under resource limits +# - watch mode refuses symlinked log files, control chars stripped from +# all parsed fields and error messages # 5.0.1: Subject parsing fixed for locally submitted mail ("from local;") # # Changes vs 4.0.9: @@ -30,11 +38,22 @@ set -euo pipefail IFS=$'\n\t' export LC_ALL=C export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +umask 077 -readonly VERSION="5.0.1" +# Environment hardening: variables that change behaviour of helper tools +unset -v AWKPATH AWKLIBPATH POSIXLY_CORRECT GAWK_READ_TIMEOUT GAWK_SOCK_RETRIES \ + GZIP XZ_OPT XZ_DEFAULTS ZSTD_CLEVEL ZSTD_NBTHREADS \ + LESS LESSOPEN LESSCLOSE LESSKEY LESSKEYIN LESSKEY_SYSTEM LESSHISTFILE \ + LESSEDIT LESSSECURE_ALLOW EDITOR VISUAL MS_RE +# As root never trust user-controlled directories (sudo -E / env_keep / SETENV) +if (( EUID == 0 )); then + unset -v TMPDIR XDG_RUNTIME_DIR +fi -die() { printf 'Error: %s\n' "$*" >&2; exit 1; } -warn() { printf 'Warning: %s\n' "$*" >&2; } +readonly VERSION="5.0.2" + +die() { local m="$*"; printf 'Error: %s\n' "${m//[[:cntrl:]]/?}" >&2; exit 1; } +warn() { local m="$*"; printf 'Warning: %s\n' "${m//[[:cntrl:]]/?}" >&2; } # ============================================================================== # 1. HELP @@ -89,7 +108,7 @@ EOF # ============================================================================== # 2. DEPENDENCY CHECK # ============================================================================== -for cmd in cat cut date find flock head mktemp sort tail uniq wc; do +for cmd in cat cut date env find flock head mktemp sort tail uniq wc; do command -v "$cmd" > /dev/null 2>&1 || die "Required command '$cmd' is not installed." done @@ -172,10 +191,35 @@ if [[ -n $TIME_FILTER && ! $TIME_FILTER =~ ^([01][0-9]|2[0-3]):([0-5]([0-9](:([0 die "Invalid --time '$TIME_FILTER'. Use \"HH:\", \"HH:MM\" or \"HH:MM:SS\"." fi +# Resource limits - validated as strings BEFORE any arithmetic use: +# bash (( )) evaluates variable contents, e.g. 'x[$(cmd)]' would execute cmd +: "${MAIL_STATS_MEM_KB:=1048576}" "${MAIL_STATS_CPU_SEC:=300}" "${MAIL_STATS_FSIZE_KB:=1048576}" +for lim in MAIL_STATS_MEM_KB MAIL_STATS_CPU_SEC MAIL_STATS_FSIZE_KB; do + [[ ${!lim} =~ ^[1-9][0-9]{0,11}$ ]] || die "$lim must be a positive integer." +done +readonly MAIL_STATS_MEM_KB MAIL_STATS_CPU_SEC MAIL_STATS_FSIZE_KB + +# Run a command with resource limits (parser/sort only, not journalctl/gzip) +run_limited() { + ( + ulimit -v "$MAIL_STATS_MEM_KB" + if (( ! WATCH_MODE )); then + ulimit -t "$MAIL_STATS_CPU_SEC" + ulimit -f "$MAIL_STATS_FSIZE_KB" + fi + exec "$@" + ) +} + +readonly MAX_FILTER_LEN=256 +for flt in "$FROM_FILTER" "$TO_FILTER" "$IP_FILTER" "$STATUS_FILTER" "$ID_FILTER"; do + (( ${#flt} <= MAX_FILTER_LEN )) || die "Filter too long (max $MAX_FILTER_LEN characters)." +done + if (( REGEX_MODE )); then for re in "$FROM_FILTER" "$TO_FILTER" "$IP_FILTER" "$STATUS_FILTER" "$ID_FILTER"; do [[ -z $re ]] && continue - MS_RE=$re "$AWK" 'BEGIN { if ("" ~ ENVIRON["MS_RE"]) {} }' 2> /dev/null \ + MS_RE=$re WATCH_MODE=0 run_limited "$AWK" 'BEGIN { if ("" ~ ENVIRON["MS_RE"]) {} }' 2> /dev/null \ || die "Invalid regular expression: $re" done fi @@ -191,20 +235,24 @@ if [[ -t 1 && -z ${NO_COLOR:-} ]] && (( ! CSV_MODE )); then fi if (( USE_COLOR )); then C_Y=$'\033[1;33m'; C_H=$'\033[1;36m'; C_R=$'\033[0m'; else C_Y=""; C_H=""; C_R=""; fi -: "${MAIL_STATS_MEM_KB:=1048576}" "${MAIL_STATS_CPU_SEC:=300}" "${MAIL_STATS_FSIZE_KB:=1048576}" -need_uint MAIL_STATS_MEM_KB "$MAIL_STATS_MEM_KB" -need_uint MAIL_STATS_CPU_SEC "$MAIL_STATS_CPU_SEC" -(( MAIL_STATS_CPU_SEC > 0 && MAIL_STATS_MEM_KB > 0 && MAIL_STATS_FSIZE_KB > 0 )) || die "MAIL_STATS_* limits must be > 0." -need_uint MAIL_STATS_FSIZE_KB "$MAIL_STATS_FSIZE_KB" # ============================================================================== # 4. LOCKING & TEMP FILES # ============================================================================== -LOCK_UID=${SUDO_UID:-$UID} -if (( UID == 0 )) && [[ -d /run/lock && -w /run/lock ]]; then - LOCK_DIR=/run/lock +if (( EUID == 0 )); then + # private root-only directory: no symlink/pre-created lock file tricks + LOCK_DIR=/run/mail-stats + [[ -L $LOCK_DIR ]] && die "$LOCK_DIR is a symlink, refusing to use it." + if [[ ! -d $LOCK_DIR ]]; then + mkdir -m 0700 -- "$LOCK_DIR" || die "Cannot create $LOCK_DIR." + fi + [[ -O $LOCK_DIR && ! -L $LOCK_DIR ]] || die "$LOCK_DIR is not owned by root, refusing to use it." + chmod 0700 -- "$LOCK_DIR" + LOCK_UID=${SUDO_UID:-0} + [[ $LOCK_UID =~ ^[0-9]+$ ]] || LOCK_UID=0 else LOCK_DIR=${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}} + LOCK_UID=$UID fi if (( WATCH_MODE )); then @@ -223,17 +271,6 @@ fi trap 'exit 130' INT trap 'exit 143' TERM -# Run a command with resource limits (parser/sort only, not journalctl/gzip) -run_limited() { - ( - ulimit -v "$MAIL_STATS_MEM_KB" - if (( ! WATCH_MODE )); then - ulimit -t "$MAIL_STATS_CPU_SEC" - ulimit -f "$MAIL_STATS_FSIZE_KB" - fi - exec "$@" - ) -} # ============================================================================== # 5. LOG DISCOVERY & STREAMING @@ -320,8 +357,8 @@ stream_logs() { stream_watch() { local active="" - if [[ -f $LOG_DIR/maillog ]]; then active=$LOG_DIR/maillog - elif [[ -f $LOG_DIR/mail.log ]]; then active=$LOG_DIR/mail.log + if [[ -f $LOG_DIR/maillog && ! -L $LOG_DIR/maillog ]]; then active=$LOG_DIR/maillog + elif [[ -f $LOG_DIR/mail.log && ! -L $LOG_DIR/mail.log ]]; then active=$LOG_DIR/mail.log fi if [[ -n $active ]]; then printf 'Starting Live Watch Mode on: %s (Ctrl+C to stop)...\n' "$active" >&2 @@ -496,7 +533,7 @@ function handle_reject(q, p, r, f, t, ip, ss, re, dsn, st, rec) { ip = (q != "NOQUEUE" && (q in c_ip)) ? c_ip[q] : "unknown" if (match(r, /^[A-Z-]+ from [^ ]*\[[^]]*\]: /)) { ss = substr(r, 1, RLENGTH); r = substr(r, RLENGTH + 1) - sub(/\]: $/, "", ss); sub(/^.*\[/, "", ss); ip = ss + sub(/\]: $/, "", ss); sub(/^.*\[/, "", ss); ip = clean(ss, 64) } f = "-" if (match($0, /from=<[^>]*>/)) { f = substr($0, RSTART + 6, RLENGTH - 7); f = (f == "") ? "<>" : clean(f, 0) } @@ -545,7 +582,7 @@ function amavis( q1, q2, ip) { if (index($0, qid ": client=")) { if (match($0, /client=[^ ,]*\[[^]]*\]/)) { ip = substr($0, RSTART, RLENGTH); sub(/^[^[]*\[/, "", ip); sub(/\]$/, "", ip) - if (!(qid in c_ip) || is_loop(c_ip[qid])) c_ip[qid] = ip + if (!(qid in c_ip) || is_loop(c_ip[qid])) c_ip[qid] = clean(ip, 64) } if (!(qid in c_time)) c_time[qid] = l_ts next @@ -575,7 +612,7 @@ function amavis( q1, q2, ip) { next } to = "-"; if (match($0, / to=<[^>]*>/)) to = clean(substr($0, RSTART + 5, RLENGTH - 6), 0) - relay = "-"; if (match($0, /relay=[^ ,]+/)) { relay = substr($0, RSTART + 6, RLENGTH - 6); sub(/:[0-9]+$/, "", relay) } + relay = "-"; if (match($0, /relay=[^ ,]+/)) { relay = substr($0, RSTART + 6, RLENGTH - 6); sub(/:[0-9]+$/, "", relay); relay = clean(relay, 255) } st = "-"; if (match($0, /status=[a-z]+/)) st = substr($0, RSTART + 7, RLENGTH - 7) dsn = "-"; if (match($0, /dsn=[0-9.]+/)) dsn = substr($0, RSTART + 4, RLENGTH - 4) reason = "-" @@ -638,8 +675,8 @@ unset selftest_out use_pager() { if [[ -t 1 ]] && (( ! CSV_MODE && ! WATCH_MODE )) && command -v less > /dev/null 2>&1; then - # LESSSECURE: no !shell, |pipe, :e, v, LESSOPEN (important when run via sudo) - env -u LESSOPEN -u LESSCLOSE LESSSECURE=1 less -R -F -X + # clean env + LESSSECURE: no !shell, |pipe, :e, v, LESSOPEN, history file + env -i TERM="${TERM:-dumb}" LESSCHARSET=utf-8 LESSSECURE=1 LESSHISTFILE=- less -R -F -X else cat fi @@ -759,4 +796,4 @@ else fi ps=("${PIPESTATUS[@]}") set -e -check_pipe "${ps[@]}" \ No newline at end of file +check_pipe "${ps[@]}"