#!/bin/bash set -euo pipefail umask 077 LOG_DIR="/var/log/BastionGuard" LOG_FILE="$LOG_DIR/phishing_updater.log" PHISH_SYS="/usr/share/BastionGuard/data/phishing" DNSMASQ_CONF="/etc/dnsmasq.d/BastionGuard-blacklist.conf" DATE=$(date '+%Y-%m-%d %H:%M:%S') if [[ ${EUID} -ne 0 ]]; then echo "BastionGuard-phishing-updater: must run as root via pkexec" >&2 exit 77 fi install -d -o root -g root -m 0755 "$LOG_DIR" "$PHISH_SYS" echo "[$DATE] Avvio aggiornamento blacklist..." >> "$LOG_FILE" # The systemd timer may invoke the helper without staged input. Preserve the # old no-op behaviour, but never consume predictable files from /tmp. if [[ $# -eq 0 ]]; then echo "[$DATE] Nessun staging directory fornita; nessun aggiornamento da installare." >> "$LOG_FILE" exit 0 fi if [[ $# -ne 1 ]]; then echo "Uso: $0 " >&2 exit 64 fi STAGE="$1" CALLER_UID="${PKEXEC_UID:-${SUDO_UID:-}}" if [[ -z "$CALLER_UID" || ! "$CALLER_UID" =~ ^[0-9]+$ || "$CALLER_UID" -eq 0 ]]; then echo "BastionGuard-phishing-updater: caller UID non affidabile" >&2 exit 65 fi if [[ -L "$STAGE" || ! -d "$STAGE" ]]; then echo "BastionGuard-phishing-updater: staging directory non valida" >&2 exit 66 fi STAGE_REAL=$(realpath -e -- "$STAGE") case "$STAGE_REAL" in /tmp/bastionguard-phish.*) ;; *) echo "BastionGuard-phishing-updater: staging path non autorizzato" >&2; exit 67 ;; esac if [[ $(stat -c '%u' -- "$STAGE_REAL") != "$CALLER_UID" || \ $(stat -c '%a' -- "$STAGE_REAL") != "700" ]]; then echo "BastionGuard-phishing-updater: owner/mode staging non validi" >&2 exit 68 fi validate_stage_file() { local f="$1" [[ -f "$f" && ! -L "$f" ]] || return 1 [[ $(stat -c '%u' -- "$f") == "$CALLER_UID" ]] || return 1 [[ $(stat -c '%a' -- "$f") == "600" ]] || return 1 } BLACKLIST="$STAGE_REAL/blacklist.txt" BLACKLIST_REDUCE="$STAGE_REAL/blacklist-reduce.txt" DNSMASQ_STAGE="$STAGE_REAL/dnsmasq-blacklist.conf" validate_stage_file "$BLACKLIST" || { echo "blacklist staging non valida" >&2; exit 69; } validate_stage_file "$BLACKLIST_REDUCE" || { echo "blacklist-reduce staging non valida" >&2; exit 69; } validate_stage_file "$DNSMASQ_STAGE" || { echo "dnsmasq staging non valida" >&2; exit 69; } # Snapshot the caller-owned inputs into a root-only directory before parsing. # The final install is always performed from this immutable snapshot, so a # caller cannot race validation by swapping the original /tmp files. install -d -o root -g root -m 0700 /var/lib/BastionGuard ROOT_STAGE=$(mktemp -d /var/lib/BastionGuard/phishing-stage.XXXXXX) trap 'rm -rf -- "$ROOT_STAGE"' EXIT install -m 0600 -- "$BLACKLIST" "$ROOT_STAGE/blacklist.txt" install -m 0600 -- "$BLACKLIST_REDUCE" "$ROOT_STAGE/blacklist-reduce.txt" install -m 0600 -- "$DNSMASQ_STAGE" "$ROOT_STAGE/dnsmasq-blacklist.conf" BLACKLIST="$ROOT_STAGE/blacklist.txt" BLACKLIST_REDUCE="$ROOT_STAGE/blacklist-reduce.txt" DNSMASQ_STAGE="$ROOT_STAGE/dnsmasq-blacklist.conf" validate_blacklist() { local f="$1" line while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" || "$line" == \#* ]] && continue [[ ${#line} -le 4096 ]] || return 1 # Backend.cpp emits URLs, domains or IPs only. Reject control/whitespace # and arbitrary file content before it can become world-readable. if [[ "$line" =~ ^https?://[^[:space:][:cntrl:]]+$ ]]; then continue fi if [[ "$line" =~ ^[A-Za-z0-9._:-]+$ && "$line" == *.* ]]; then continue fi return 1 done < "$f" } validate_blacklist "$BLACKLIST" || { echo "blacklist contenuto non valido" >&2; exit 70; } validate_blacklist "$BLACKLIST_REDUCE" || { echo "blacklist-reduce contenuto non valido" >&2; exit 70; } # Only the directive generated by Backend.cpp is accepted. This blocks an # attacker from smuggling dhcp-script= or any other executable dnsmasq option. while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" ]] && continue if [[ ! "$line" =~ ^address=/[A-Za-z0-9.-]+/127\.0\.0\.2$ ]]; then echo "BastionGuard-phishing-updater: direttiva dnsmasq rifiutata" >&2 exit 71 fi done < "$DNSMASQ_STAGE" install -m 0644 -- "$BLACKLIST" "$PHISH_SYS/blacklist.txt" install -m 0644 -- "$BLACKLIST_REDUCE" "$PHISH_SYS/blacklist-reduce.txt" echo "[$DATE] blacklist installate" >> "$LOG_FILE" # Copy the caller's whitelist only if it is a regular, caller-owned file. REAL_HOME=$(getent passwd "$CALLER_UID" | cut -d: -f6) if [[ -n "$REAL_HOME" ]]; then WL_TXT="$REAL_HOME/.local/share/BastionGuard/phishing/whitelist.txt" if [[ -f "$WL_TXT" && ! -L "$WL_TXT" && $(stat -c '%u' -- "$WL_TXT") == "$CALLER_UID" ]]; then install -m 0600 -- "$WL_TXT" "$ROOT_STAGE/whitelist.txt" WL_OK=1 while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" || "$line" == \#* ]] && continue if [[ ! "$line" =~ ^[A-Za-z0-9.-]+$ ]]; then WL_OK=0; break; fi done < "$ROOT_STAGE/whitelist.txt" if [[ "$WL_OK" -eq 1 ]]; then install -m 0644 -- "$ROOT_STAGE/whitelist.txt" "$PHISH_SYS/whitelist.txt" echo "[$DATE] whitelist utente copiata in sys" >> "$LOG_FILE" fi fi fi install -d -o root -g root -m 0755 /etc/dnsmasq.d install -m 0644 -- "$DNSMASQ_STAGE" "$DNSMASQ_CONF" DNSMASQ_OK=0 if dnsmasq --test > "$LOG_DIR/dnsmasq-test.log" 2>&1; then DNSMASQ_OK=1 else echo "[$DATE] dnsmasq --test fallito, skip reload" >> "$LOG_FILE" fi SCANNER_ACTIVE=0 systemctl is-active --quiet BastionGuard-phishing-scanner.service && SCANNER_ACTIVE=1 || true if [[ "$DNSMASQ_OK" -eq 1 ]]; then systemctl restart dnsmasq echo "[$DATE] dnsmasq ricaricato" >> "$LOG_FILE" fi if [[ "$SCANNER_ACTIVE" -eq 1 ]]; then systemctl restart BastionGuard-phishing-scanner.service echo "[$DATE] Scanner riavviato" >> "$LOG_FILE" fi echo "[$DATE] Aggiornamento completato" >> "$LOG_FILE" exit 0