security: SSRF hardening, log injection sanitization, and allow_local_smtp

Add SSRF host validation to maintenance.py send_nightly_email and
scheduler.py send_zombie_alert_email using validate_external_url().
New allow_local_smtp config key permits private-IP SMTP for local
relay setups.

Add sanitize_name() to security_utils and apply it to all log calls
in message_handler, repeater_manager, path_command, solarforecast_command,
command_manager, and discord_bridge_service to prevent log injection.

Move nightly email logic from duplicate scheduler._send_nightly_email()
into the canonical maintenance.py implementation, removing the duplicate.
Update tests to call maintenance.send_nightly_email() directly.

Add validate_external_url allow_private parameter with support for
loopback, RFC1918, CGN, and link-local address ranges.
This commit is contained in:
Stacy Olivas
2026-04-14 10:02:36 -07:00
committed by agessaman
parent c7fa0ba3d2
commit 54aeb28bf0
18 changed files with 707 additions and 80 deletions
+4 -4
View File
@@ -15,7 +15,7 @@ from typing import Any, Optional
from .enums import AdvertFlags, DeviceRole, PayloadType, PayloadVersion, RouteType
from .graph_trace_helper import update_mesh_graph_from_trace_data
from .models import MeshMessage
from .security_utils import sanitize_input
from .security_utils import sanitize_input, sanitize_name
from .utils import (
calculate_packet_hash,
decode_path_len_byte,
@@ -147,7 +147,7 @@ class MessageHandler:
self.logger.debug(f"Payload keys: {list(payload.keys())}")
self.logger.debug(f"Event metadata: {event.metadata if hasattr(event, 'metadata') else 'None'}")
self.logger.info(f"Received DM from {payload.get('pubkey_prefix', 'unknown')}: {payload.get('text', '')}")
self.logger.info(f"Received DM from {sanitize_name(payload.get('pubkey_prefix', 'unknown'))}: {sanitize_name(payload.get('text', ''))}")
# Extract path information from contacts using pubkey_prefix
path_info = "Unknown"
@@ -510,7 +510,7 @@ class MessageHandler:
parsed_advert = self.parse_advert(payload_bytes)
if parsed_advert:
advert_data = parsed_advert
self.logger.info(f"✅ Parsed ADVERT: {advert_data.get('mode', 'Unknown')} - {advert_data.get('name', 'No name')}")
self.logger.info(f"✅ Parsed ADVERT: {sanitize_name(advert_data.get('mode', 'Unknown'))} - {sanitize_name(advert_data.get('name', 'No name'))}")
except Exception as e:
self.logger.warning(f"Failed to parse ADVERT payload: {e}")
@@ -640,7 +640,7 @@ class MessageHandler:
self.logger.info(f"📡 Tracked {mode}: {name}{location}{hop_info}")
else:
self.logger.warning(f"Failed to track contact advertisement: {advert_data.get('name', 'Unknown')}")
self.logger.warning(f"Failed to track contact advertisement: {sanitize_name(advert_data.get('name', 'Unknown'))}")
except Exception as e:
self.logger.error(f"Error processing advertisement packet: {e}")