Fix HIGH severity security and thread-safety issues

Daemon fixes:
- Add _db_lock for thread-safe SQLite access
- Atomic SIGHUP config swap (build all values before applying)
- Check world-writable permission before loading pickle model
- Write model files with 0o600 permissions via os.open
- Module-level xdp_common import with fatal exit on failure
- Close traffic DB on shutdown
- Add period_data parameter to _train() to avoid race condition

CLI fixes:
- Replace $COMMON_PY variable with hardcoded 'xdp_common'
- Pass CONFIG_FILE via sys.argv instead of string interpolation
- Add key_hex regex validation before all bpftool commands
- Switch sanitize_input from denylist to strict allowlist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kaffa
2026-02-07 11:22:55 +09:00
parent a6519fd664
commit 2c29eab991
2 changed files with 125 additions and 86 deletions

View File

@@ -14,7 +14,6 @@ COUNTRY_DIR="/etc/xdp-blocker/countries"
GEOIP_DB="/usr/share/GeoIP/GeoLite2-Country.mmdb"
CITY_DB="/usr/share/GeoIP/GeoLite2-City.mmdb"
ASN_DB="/usr/share/GeoIP/GeoLite2-ASN.mmdb"
COMMON_PY="xdp_common"
# Ensure Python can find xdp_common.py (installed to /usr/local/bin)
export PYTHONPATH="/usr/local/bin:${PYTHONPATH:-}"
@@ -30,10 +29,11 @@ log_ok() { echo -e "${GREEN}[OK]${NC} $1"; logger -t xdp-defense "OK: $1" 2>/d
log_err() { echo -e "${RED}[ERROR]${NC} $1" >&2; logger -t xdp-defense -p user.err "ERROR: $1" 2>/dev/null || true; }
log_info() { echo -e "${CYAN}[INFO]${NC} $1"; logger -t xdp-defense "INFO: $1" 2>/dev/null || true; }
# Sanitize input for safe embedding in Python strings (reject dangerous chars)
# Sanitize input - strict allowlist for IP addresses, CIDR notation, preset names
sanitize_input() {
local val="$1"
if [[ "$val" =~ [\'\"\;\$\`\\] ]]; then
# Allow only alphanumeric, dots, colons, slashes, hyphens, underscores
if [[ ! "$val" =~ ^[a-zA-Z0-9._:/\ -]+$ ]]; then
log_err "Invalid characters in input: $val"
return 1
fi
@@ -44,10 +44,10 @@ get_iface() {
if [ -f "$CONFIG_FILE" ]; then
local iface
iface=$(python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
import yaml, sys
with open(sys.argv[1]) as f:
print(yaml.safe_load(f).get('general',{}).get('interface','eth0'))
" 2>/dev/null)
" "$CONFIG_FILE" 2>/dev/null)
echo "${iface:-eth0}"
else
echo "eth0"
@@ -249,7 +249,7 @@ cmd_status() {
# Rate config
python3 -c "
from ${COMMON_PY} import read_rate_config
from xdp_common import read_rate_config
cfg = read_rate_config()
if cfg:
print(f'Rate limit: {cfg[\"pps_threshold\"]} pps (window: {cfg[\"window_ns\"] // 1_000_000_000}s)')
@@ -259,7 +259,7 @@ else:
# Blocked IPs
python3 -c "
from ${COMMON_PY} import dump_blocked_ips
from xdp_common import dump_blocked_ips
v4 = dump_blocked_ips('blocked_ips_v4')
v6 = dump_blocked_ips('blocked_ips_v6')
print(f'Blocked IPs: {len(v4) + len(v6)}')
@@ -283,8 +283,9 @@ cmd_blocker_add() {
[ -z "$map_id" ] && { log_err "IPv6 map not found. Is XDP loaded?"; exit 1; }
local key_hex
key_hex=$(python3 -c "import sys; from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$cidr" 2>/dev/null)
key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$cidr" 2>/dev/null)
[ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; exit 1; }
[[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null
[ "$2" != "quiet" ] && log_ok "Added (v6): $cidr" || true
@@ -312,6 +313,7 @@ cmd_blocker_add() {
IFS='.' read -r a b c d <<< "$ip"
local key_hex
key_hex=$(printf '%02x 00 00 00 %02x %02x %02x %02x' "$prefix" "$a" "$b" "$c" "$d")
[[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null
[ "$2" != "quiet" ] && log_ok "Added: $cidr" || true
@@ -331,8 +333,9 @@ cmd_blocker_del() {
[ -z "$map_id" ] && { log_err "IPv6 map not found"; exit 1; }
local key_hex
key_hex=$(python3 -c "import sys; from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$cidr" 2>/dev/null)
key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$cidr" 2>/dev/null)
[ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; exit 1; }
[[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed (v6): $cidr"
local tmpfile="${BLOCKLIST_FILE}.tmp.$$"
@@ -358,6 +361,7 @@ cmd_blocker_del() {
IFS='.' read -r a b c d <<< "$ip"
local key_hex
key_hex=$(printf '%02x 00 00 00 %02x %02x %02x %02x' "$prefix" "$a" "$b" "$c" "$d")
[[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed: $cidr"
local tmpfile="${BLOCKLIST_FILE}.tmp.$$"
@@ -461,12 +465,13 @@ cmd_whitelist_add() {
local map_name key_hex
if [[ "$name" == *":"* ]]; then
map_name="whitelist_v6"
key_hex=$(python3 -c "import sys; from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$name" 2>/dev/null)
key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$name" 2>/dev/null)
else
map_name="whitelist_v4"
key_hex=$(python3 -c "import sys; from ${COMMON_PY} import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null)
key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null)
fi
[ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; exit 1; }
[[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
local map_id
map_id=$(get_map_id "$map_name")
@@ -496,12 +501,13 @@ cmd_whitelist_del() {
local map_name key_hex
if [[ "$name" == *":"* ]]; then
map_name="whitelist_v6"
key_hex=$(python3 -c "import sys; from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$name" 2>/dev/null)
key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$name" 2>/dev/null)
else
map_name="whitelist_v4"
key_hex=$(python3 -c "import sys; from ${COMMON_PY} import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null)
key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null)
fi
[ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; exit 1; }
[[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
local map_id
map_id=$(get_map_id "$map_name")
@@ -532,7 +538,7 @@ cmd_whitelist_list() {
cmd_ddos_stats() {
echo -e "${BOLD}=== DDoS Statistics ===${NC}"
python3 -c "
from ${COMMON_PY} import read_percpu_stats
from xdp_common import read_percpu_stats
stats = read_percpu_stats('global_stats', 5)
labels = ['Passed', 'Dropped (blocked)', 'Dropped (rate)', 'Total', 'Errors']
for i, label in enumerate(labels):
@@ -547,7 +553,7 @@ cmd_ddos_top() {
echo -e "${BOLD}=== Top $n IPs by Packet Count ===${NC}"
python3 -c "
import sys
from ${COMMON_PY} import dump_rate_counters
from xdp_common import dump_rate_counters
entries = dump_rate_counters('rate_counter_v4', int(sys.argv[1]))
if not entries:
print(' (empty)')
@@ -569,7 +575,7 @@ if entries6:
cmd_ddos_blocked() {
echo -e "${BOLD}=== Blocked IPs ===${NC}"
python3 -c "
from ${COMMON_PY} import dump_blocked_ips
from xdp_common import dump_blocked_ips
with open('/proc/uptime') as f:
now_ns = int(float(f.read().split()[0]) * 1_000_000_000)
@@ -604,7 +610,7 @@ cmd_ddos_block() {
[ -z "$ip" ] && { log_err "Usage: xdp-defense ddos block <ip> [duration_sec]"; exit 1; }
[[ "$duration" =~ ^[0-9]+$ ]] || { log_err "Invalid duration: $duration"; exit 1; }
python3 -c "import sys; from ${COMMON_PY} import block_ip; block_ip(sys.argv[1], int(sys.argv[2]))" "$ip" "$duration" 2>/dev/null || \
python3 -c "import sys; from xdp_common import block_ip; block_ip(sys.argv[1], int(sys.argv[2]))" "$ip" "$duration" 2>/dev/null || \
{ log_err "Failed to block $ip"; exit 1; }
if [ "$duration" -gt 0 ] 2>/dev/null; then
@@ -619,7 +625,7 @@ cmd_ddos_unblock() {
ip=$(sanitize_input "$1") || exit 1
[ -z "$ip" ] && { log_err "Usage: xdp-defense ddos unblock <ip>"; exit 1; }
python3 -c "import sys; from ${COMMON_PY} import unblock_ip; unblock_ip(sys.argv[1])" "$ip" 2>/dev/null || \
python3 -c "import sys; from xdp_common import unblock_ip; unblock_ip(sys.argv[1])" "$ip" 2>/dev/null || \
{ log_err "Failed to unblock $ip"; exit 1; }
log_ok "Unblocked $ip"
}
@@ -631,7 +637,7 @@ cmd_ddos_config() {
echo -e "${BOLD}=== Rate Configuration ===${NC}"
echo -e "\n${CYAN}Active (BPF map):${NC}"
python3 -c "
from ${COMMON_PY} import read_rate_config
from xdp_common import read_rate_config
cfg = read_rate_config()
if cfg:
pps = cfg['pps_threshold']
@@ -647,8 +653,8 @@ else:
if [ -f "$CONFIG_FILE" ]; then
echo -e "\n${CYAN}Config file ($CONFIG_FILE):${NC}"
python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
import yaml, sys
with open(sys.argv[1]) as f:
cfg = yaml.safe_load(f)
rl = cfg.get('rate_limits', {})
print(f' Default PPS: {rl.get(\"default_pps\", \"N/A\")}')
@@ -662,7 +668,7 @@ if profiles:
hours = p.get('hours', '')
pps = p.get('pps', 'N/A')
print(f' {name}: pps={pps}, hours={hours}')
" 2>/dev/null
" "$CONFIG_FILE" 2>/dev/null
fi
;;
set)
@@ -674,7 +680,7 @@ if profiles:
python3 -c "
import sys
from ${COMMON_PY} import read_rate_config, write_rate_config
from xdp_common import read_rate_config, write_rate_config
cfg = read_rate_config()
if not cfg:
cfg = {'pps_threshold': 1000, 'bps_threshold': 0, 'window_ns': 1000000000}
@@ -700,16 +706,16 @@ cmd_ddos_config_apply() {
[ ! -f "$CONFIG_FILE" ] && return
python3 -c "
import yaml
from ${COMMON_PY} import write_rate_config
with open('$CONFIG_FILE') as f:
import yaml, sys
from xdp_common import write_rate_config
with open(sys.argv[1]) as f:
cfg = yaml.safe_load(f)
rl = cfg.get('rate_limits', {})
pps = rl.get('default_pps', 1000)
bps = rl.get('default_bps', 0)
win = rl.get('window_sec', 1)
write_rate_config(pps, bps, win * 1000000000)
" 2>/dev/null || return 0
" "$CONFIG_FILE" 2>/dev/null || return 0
[ "$quiet" != "quiet" ] && log_ok "Config applied from $CONFIG_FILE" || true
}
@@ -777,8 +783,8 @@ cmd_ai_status() {
if [ -f "$CONFIG_FILE" ]; then
python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
import yaml, sys
with open(sys.argv[1]) as f:
cfg = yaml.safe_load(f)
ai = cfg.get('ai', {})
enabled = ai.get('enabled', False)
@@ -786,7 +792,7 @@ if enabled:
print(f'AI Detection: enabled ({ai.get(\"model_type\", \"IsolationForest\")})')
else:
print('AI Detection: disabled')
" 2>/dev/null
" "$CONFIG_FILE" 2>/dev/null
fi
}
@@ -804,11 +810,11 @@ cmd_ai_retrain() {
cmd_ai_traffic() {
local db_file
db_file=$(python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
import yaml, sys
with open(sys.argv[1]) as f:
cfg = yaml.safe_load(f)
print(cfg.get('ai',{}).get('traffic_log_db', '/var/lib/xdp-defense/traffic_log.db'))
" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db")
" "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db")
[ ! -f "$db_file" ] && { log_err "Traffic log not found: $db_file"; exit 1; }
@@ -872,7 +878,7 @@ conn.close()
# Show next retrain time
import yaml, os, time
try:
with open('$CONFIG_FILE') as f:
with open(sys.argv[2]) as f:
cfg = yaml.safe_load(f)
retrain_interval = cfg.get('ai',{}).get('retrain_interval', 86400)
model_file = cfg.get('ai',{}).get('model_file', '/var/lib/xdp-defense/ai_model.pkl')
@@ -890,7 +896,7 @@ try:
except:
pass
print()
" "$db_file"
" "$db_file" "$CONFIG_FILE"
}
cmd_ai_log() {
@@ -899,11 +905,11 @@ cmd_ai_log() {
local db_file
db_file=$(python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
import yaml, sys
with open(sys.argv[1]) as f:
cfg = yaml.safe_load(f)
print(cfg.get('ai',{}).get('traffic_log_db', '/var/lib/xdp-defense/traffic_log.db'))
" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db")
" "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db")
[ ! -f "$db_file" ] && { log_err "Traffic log not found: $db_file"; exit 1; }
@@ -1001,6 +1007,7 @@ cmd_geoip() {
IFS='.' read -r a b c d <<< "$ip"
local key_hex
key_hex=$(printf '20 00 00 00 %02x %02x %02x %02x' "$a" "$b" "$c" "$d")
[[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
if bpftool map lookup id "$map_id" key hex $key_hex 2>/dev/null | grep -q "value"; then
echo -e "Blocker: ${RED}BLOCKED${NC}"
else
@@ -1013,7 +1020,8 @@ cmd_geoip() {
ddos_map_id=$(get_map_id blocked_ips_v4)
if [ -n "$ddos_map_id" ]; then
local ddos_key_hex
ddos_key_hex=$(python3 -c "import sys; from ${COMMON_PY} import ip_to_hex_key; print(ip_to_hex_key(sys.argv[1]))" "$ip" 2>/dev/null)
ddos_key_hex=$(python3 -c "import sys; from xdp_common import ip_to_hex_key; print(ip_to_hex_key(sys.argv[1]))" "$ip" 2>/dev/null)
[[ "$ddos_key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; }
if [ -n "$ddos_key_hex" ] && bpftool map lookup id "$ddos_map_id" key hex $ddos_key_hex 2>/dev/null | grep -q "value"; then
echo -e "DDoS: ${RED}BLOCKED${NC}"
else