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:
1 parent
84df6d23d6
commit
b5937cc39b
1 file changed
+69
-32
+69
-32
@@ -1,11 +1,19 @@
|
|||||||
#!/bin/bash
|
#!/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),
|
# Requires: bash >= 4.2, GNU coreutils/findutils, util-linux (flock),
|
||||||
# gawk (recommended) or mawk 1.3.4 (verified by a self-test at start)
|
# gawk (recommended) or mawk 1.3.4 (verified by a self-test at start)
|
||||||
# Optional: gzip/xz/bzip2/zstd (rotated logs), journalctl, less
|
# 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;")
|
# 5.0.1: Subject parsing fixed for locally submitted mail ("from local;")
|
||||||
#
|
#
|
||||||
# Changes vs 4.0.9:
|
# Changes vs 4.0.9:
|
||||||
@@ -30,11 +38,22 @@ set -euo pipefail
|
|||||||
IFS=$'\n\t'
|
IFS=$'\n\t'
|
||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
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; }
|
readonly VERSION="5.0.2"
|
||||||
warn() { printf 'Warning: %s\n' "$*" >&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
|
# 1. HELP
|
||||||
@@ -89,7 +108,7 @@ EOF
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 2. DEPENDENCY CHECK
|
# 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."
|
command -v "$cmd" > /dev/null 2>&1 || die "Required command '$cmd' is not installed."
|
||||||
done
|
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\"."
|
die "Invalid --time '$TIME_FILTER'. Use \"HH:\", \"HH:MM\" or \"HH:MM:SS\"."
|
||||||
fi
|
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
|
if (( REGEX_MODE )); then
|
||||||
for re in "$FROM_FILTER" "$TO_FILTER" "$IP_FILTER" "$STATUS_FILTER" "$ID_FILTER"; do
|
for re in "$FROM_FILTER" "$TO_FILTER" "$IP_FILTER" "$STATUS_FILTER" "$ID_FILTER"; do
|
||||||
[[ -z $re ]] && continue
|
[[ -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"
|
|| die "Invalid regular expression: $re"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -191,20 +235,24 @@ if [[ -t 1 && -z ${NO_COLOR:-} ]] && (( ! CSV_MODE )); then
|
|||||||
fi
|
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
|
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
|
# 4. LOCKING & TEMP FILES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
LOCK_UID=${SUDO_UID:-$UID}
|
if (( EUID == 0 )); then
|
||||||
if (( UID == 0 )) && [[ -d /run/lock && -w /run/lock ]]; then
|
# private root-only directory: no symlink/pre-created lock file tricks
|
||||||
LOCK_DIR=/run/lock
|
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
|
else
|
||||||
LOCK_DIR=${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}
|
LOCK_DIR=${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}
|
||||||
|
LOCK_UID=$UID
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( WATCH_MODE )); then
|
if (( WATCH_MODE )); then
|
||||||
@@ -223,17 +271,6 @@ fi
|
|||||||
trap 'exit 130' INT
|
trap 'exit 130' INT
|
||||||
trap 'exit 143' TERM
|
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
|
# 5. LOG DISCOVERY & STREAMING
|
||||||
@@ -320,8 +357,8 @@ stream_logs() {
|
|||||||
|
|
||||||
stream_watch() {
|
stream_watch() {
|
||||||
local active=""
|
local active=""
|
||||||
if [[ -f $LOG_DIR/maillog ]]; then active=$LOG_DIR/maillog
|
if [[ -f $LOG_DIR/maillog && ! -L $LOG_DIR/maillog ]]; then active=$LOG_DIR/maillog
|
||||||
elif [[ -f $LOG_DIR/mail.log ]]; then active=$LOG_DIR/mail.log
|
elif [[ -f $LOG_DIR/mail.log && ! -L $LOG_DIR/mail.log ]]; then active=$LOG_DIR/mail.log
|
||||||
fi
|
fi
|
||||||
if [[ -n $active ]]; then
|
if [[ -n $active ]]; then
|
||||||
printf 'Starting Live Watch Mode on: %s (Ctrl+C to stop)...\n' "$active" >&2
|
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"
|
ip = (q != "NOQUEUE" && (q in c_ip)) ? c_ip[q] : "unknown"
|
||||||
if (match(r, /^[A-Z-]+ from [^ ]*\[[^]]*\]: /)) {
|
if (match(r, /^[A-Z-]+ from [^ ]*\[[^]]*\]: /)) {
|
||||||
ss = substr(r, 1, RLENGTH); r = substr(r, RLENGTH + 1)
|
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 = "-"
|
f = "-"
|
||||||
if (match($0, /from=<[^>]*>/)) { f = substr($0, RSTART + 6, RLENGTH - 7); f = (f == "") ? "<>" : clean(f, 0) }
|
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 (index($0, qid ": client=")) {
|
||||||
if (match($0, /client=[^ ,]*\[[^]]*\]/)) {
|
if (match($0, /client=[^ ,]*\[[^]]*\]/)) {
|
||||||
ip = substr($0, RSTART, RLENGTH); sub(/^[^[]*\[/, "", ip); sub(/\]$/, "", ip)
|
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
|
if (!(qid in c_time)) c_time[qid] = l_ts
|
||||||
next
|
next
|
||||||
@@ -575,7 +612,7 @@ function amavis( q1, q2, ip) {
|
|||||||
next
|
next
|
||||||
}
|
}
|
||||||
to = "-"; if (match($0, / to=<[^>]*>/)) to = clean(substr($0, RSTART + 5, RLENGTH - 6), 0)
|
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)
|
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)
|
dsn = "-"; if (match($0, /dsn=[0-9.]+/)) dsn = substr($0, RSTART + 4, RLENGTH - 4)
|
||||||
reason = "-"
|
reason = "-"
|
||||||
@@ -638,8 +675,8 @@ unset selftest_out
|
|||||||
|
|
||||||
use_pager() {
|
use_pager() {
|
||||||
if [[ -t 1 ]] && (( ! CSV_MODE && ! WATCH_MODE )) && command -v less > /dev/null 2>&1; then
|
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)
|
# clean env + LESSSECURE: no !shell, |pipe, :e, v, LESSOPEN, history file
|
||||||
env -u LESSOPEN -u LESSCLOSE LESSSECURE=1 less -R -F -X
|
env -i TERM="${TERM:-dumb}" LESSCHARSET=utf-8 LESSSECURE=1 LESSHISTFILE=- less -R -F -X
|
||||||
else
|
else
|
||||||
cat
|
cat
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in new issue
Block a user