update mail-status

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
This commit is contained in:
robott committed 2026-09-15 17:19:50 +02:00
1 parent 84df6d23d6
commit b5937cc39b
1 file changed
+69 -32
+69 -32
View File
@@ -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