Fix PR #124 remaining issues: XSS, state management, DB regression

- kill_all() now resets gsm_spy_scanner_running and related state so
  the scanner thread stops after killall
- scanner_thread sets flag to False instead of None on exit
- Restore alert_rules, alert_events, recording_sessions tables and
  wifi_clients column removed by PR in database.py
- Escape all server-sourced values in analysis modals with escapeHtml()
- Reset gsm_towers_found/gsm_devices_tracked on stop to prevent
  counter drift across sessions
- Replace raw terminate/kill with safe_terminate() in scanner_thread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-08 15:02:14 +00:00
parent bdba56bef1
commit f6c19af33a
4 changed files with 96 additions and 56 deletions
+6
View File
@@ -676,6 +676,7 @@ def kill_all() -> Response:
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
global gsm_spy_livemon_process, gsm_spy_monitor_process
global gsm_spy_scanner_running, gsm_spy_active_device, gsm_spy_selected_arfcn, gsm_spy_region
# Import adsb and ais modules to reset their state
from routes import adsb as adsb_module
@@ -754,6 +755,11 @@ def kill_all() -> Response:
# Reset GSM Spy state
with gsm_spy_lock:
gsm_spy_scanner_running = False
gsm_spy_active_device = None
gsm_spy_selected_arfcn = None
gsm_spy_region = 'Americas'
if gsm_spy_livemon_process:
try:
if safe_terminate(gsm_spy_livemon_process):
+7 -30
View File
@@ -463,7 +463,7 @@ def start_monitor():
@gsm_spy_bp.route('/stop', methods=['POST'])
def stop_scanner():
"""Stop GSM scanner and monitor."""
global gsm_connected
global gsm_connected, gsm_towers_found, gsm_devices_tracked
with app_module.gsm_spy_lock:
killed = []
@@ -492,6 +492,8 @@ def stop_scanner():
app_module.gsm_spy_active_device = None
app_module.gsm_spy_selected_arfcn = None
gsm_connected = False
gsm_towers_found = 0
gsm_devices_tracked = 0
return jsonify({'status': 'stopped', 'killed': killed})
@@ -1317,13 +1319,7 @@ def scanner_thread(cmd, device_index):
# Clean up process with timeout
if process.poll() is None:
logger.info("Terminating scanner process")
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
logger.warning("Process didn't terminate, killing")
process.kill()
process.wait()
safe_terminate(process, timeout=5)
else:
process.wait() # Reap zombie
@@ -1332,14 +1328,7 @@ def scanner_thread(cmd, device_index):
except Exception as e:
logger.error(f"Scanner scan error: {e}", exc_info=True)
if process and process.poll() is None:
try:
process.terminate()
process.wait(timeout=2)
except Exception:
try:
process.kill()
except Exception:
pass
safe_terminate(process)
# Check if should continue
if not app_module.gsm_spy_scanner_running:
@@ -1358,25 +1347,13 @@ def scanner_thread(cmd, device_index):
finally:
# Always cleanup
if process and process.poll() is None:
try:
process.terminate()
process.wait(timeout=5)
except Exception:
try:
process.kill()
process.wait()
except Exception:
pass
# Unregister process from cleanup list
if process:
unregister_process(process)
safe_terminate(process, timeout=5)
logger.info("Scanner thread terminated")
# Reset global state
with app_module.gsm_spy_lock:
app_module.gsm_spy_scanner_running = None
app_module.gsm_spy_scanner_running = False
if app_module.gsm_spy_active_device is not None:
from app import release_sdr_device
release_sdr_device(app_module.gsm_spy_active_device)
+24 -24
View File
@@ -2160,14 +2160,14 @@
const velocity_kmh = (item.estimated_velocity * 3.6).toFixed(2);
html += `
<div class="analysis-device-item">
<div style="font-weight: 600; color: var(--accent-cyan);">${item.device_id}</div>
<div style="font-weight: 600; color: var(--accent-cyan);">${escapeHtml(item.device_id)}</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Velocity:</span>
<span class="analysis-stat-value">${velocity_kmh} km/h</span>
<span class="analysis-stat-value">${escapeHtml(velocity_kmh)} km/h</span>
</div>
<div class="analysis-stat">
<span class="analysis-stat-label">TA Change:</span>
<span class="analysis-stat-value">${item.prev_ta}${item.curr_ta}</span>
<span class="analysis-stat-value">${escapeHtml(String(item.prev_ta))}${escapeHtml(String(item.curr_ta))}</span>
</div>
<div style="font-size: 9px; color: var(--text-dim); margin-top: 4px;">${new Date(item.timestamp).toLocaleString()}</div>
</div>
@@ -2201,18 +2201,18 @@
item.density_level === 'medium' ? 'var(--accent-yellow)' : 'var(--accent-green)';
html += `
<div class="analysis-device-item" style="border-left-color: ${densityColor};">
<div style="font-weight: 600; color: var(--accent-cyan);">Cell ${item.cid}</div>
<div style="font-weight: 600; color: var(--accent-cyan);">Cell ${escapeHtml(String(item.cid))}</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Unique Devices:</span>
<span class="analysis-stat-value">${item.unique_devices}</span>
<span class="analysis-stat-value">${escapeHtml(String(item.unique_devices))}</span>
</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Total Pings:</span>
<span class="analysis-stat-value">${item.total_pings}</span>
<span class="analysis-stat-value">${escapeHtml(String(item.total_pings))}</span>
</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Density:</span>
<span class="analysis-stat-value" style="color: ${densityColor}; text-transform: uppercase;">${item.density_level}</span>
<span class="analysis-stat-value" style="color: ${densityColor}; text-transform: uppercase;">${escapeHtml(item.density_level)}</span>
</div>
</div>
`;
@@ -2241,25 +2241,25 @@
const data = await response.json();
if (data.error) {
contentDiv.innerHTML = `<div class="analysis-warning">${data.error}</div>`;
contentDiv.innerHTML = `<div class="analysis-warning">${escapeHtml(data.error)}</div>`;
} else if (data.regular_locations && data.regular_locations.length > 0) {
let html = `
<div style="font-size: 10px; color: var(--text-secondary); margin-bottom: 10px;">
${data.total_observations} total observations
${escapeHtml(String(data.total_observations))} total observations
</div>
<div style="font-weight: 600; margin-bottom: 8px;">Regular Locations:</div>
`;
data.regular_locations.forEach(loc => {
html += `
<div class="analysis-device-item">
<div style="font-weight: 600; color: var(--accent-cyan);">Cell ${loc.cid}</div>
<div style="font-weight: 600; color: var(--accent-cyan);">Cell ${escapeHtml(String(loc.cid))}</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Typical Time:</span>
<span class="analysis-stat-value">${loc.typical_time}</span>
<span class="analysis-stat-value">${escapeHtml(loc.typical_time)}</span>
</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Frequency:</span>
<span class="analysis-stat-value">${loc.frequency} times</span>
<span class="analysis-stat-value">${escapeHtml(String(loc.frequency))} times</span>
</div>
</div>
`;
@@ -2290,17 +2290,17 @@
const data = await response.json();
if (data.error) {
contentDiv.innerHTML = `<div class="analysis-warning">${data.error}</div>`;
contentDiv.innerHTML = `<div class="analysis-warning">${escapeHtml(data.error)}</div>`;
} else {
const statusColor = data.status === 'suspicious' ? 'var(--accent-red)' : 'var(--accent-green)';
let html = `
<div class="analysis-stat">
<span class="analysis-stat-label">Status:</span>
<span class="analysis-stat-value" style="color: ${statusColor}; text-transform: uppercase;">${data.status}</span>
<span class="analysis-stat-value" style="color: ${statusColor}; text-transform: uppercase;">${escapeHtml(data.status)}</span>
</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Neighbor Count:</span>
<span class="analysis-stat-value">${data.neighbor_count}</span>
<span class="analysis-stat-value">${escapeHtml(String(data.neighbor_count))}</span>
</div>
`;
@@ -2309,8 +2309,8 @@
data.issues.forEach(issue => {
html += `
<div class="analysis-warning">
<div style="font-weight: 600;">${issue.type}</div>
<div>${issue.message}</div>
<div style="font-weight: 600;">${escapeHtml(issue.type)}</div>
<div>${escapeHtml(issue.message)}</div>
</div>
`;
});
@@ -2342,15 +2342,15 @@
const data = await response.json();
if (data.error) {
contentDiv.innerHTML = `<div class="analysis-warning">${data.error}</div>`;
contentDiv.innerHTML = `<div class="analysis-warning">${escapeHtml(data.error)}</div>`;
} else {
let html = `
<div style="font-size: 10px; color: var(--text-secondary); margin-bottom: 10px;">
Last ${data.time_window_minutes} minutes
Last ${escapeHtml(String(data.time_window_minutes))} minutes
</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Active Devices:</span>
<span class="analysis-stat-value">${data.active_devices}</span>
<span class="analysis-stat-value">${escapeHtml(String(data.active_devices))}</span>
</div>
`;
@@ -2361,16 +2361,16 @@
corr.activity_level === 'medium' ? 'var(--accent-yellow)' : 'var(--accent-green)';
html += `
<div class="analysis-device-item">
<div style="font-weight: 600; color: var(--accent-cyan);">${corr.device_id}</div>
<div style="font-weight: 600; color: var(--accent-cyan);">${escapeHtml(corr.device_id)}</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Burst Count:</span>
<span class="analysis-stat-value">${corr.burst_count}</span>
<span class="analysis-stat-value">${escapeHtml(String(corr.burst_count))}</span>
</div>
<div class="analysis-stat">
<span class="analysis-stat-label">Activity:</span>
<span class="analysis-stat-value" style="color: ${activityColor}; text-transform: uppercase;">${corr.activity_level}</span>
<span class="analysis-stat-value" style="color: ${activityColor}; text-transform: uppercase;">${escapeHtml(corr.activity_level)}</span>
</div>
<div style="font-size: 9px; color: var(--text-dim); margin-top: 4px;">TA: ${corr.ta_value}</div>
<div style="font-size: 9px; color: var(--text-dim); margin-top: 4px;">TA: ${escapeHtml(String(corr.ta_value))}</div>
</div>
`;
});
+59 -2
View File
@@ -102,6 +102,52 @@ def init_db() -> None:
)
''')
# Alert rules
conn.execute('''
CREATE TABLE IF NOT EXISTS alert_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
mode TEXT,
event_type TEXT,
match TEXT,
severity TEXT DEFAULT 'medium',
enabled BOOLEAN DEFAULT 1,
notify TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Alert events
conn.execute('''
CREATE TABLE IF NOT EXISTS alert_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_id INTEGER,
mode TEXT,
event_type TEXT,
severity TEXT DEFAULT 'medium',
title TEXT,
message TEXT,
payload TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (rule_id) REFERENCES alert_rules(id) ON DELETE SET NULL
)
''')
# Session recordings
conn.execute('''
CREATE TABLE IF NOT EXISTS recording_sessions (
id TEXT PRIMARY KEY,
mode TEXT NOT NULL,
label TEXT,
started_at TIMESTAMP NOT NULL,
stopped_at TIMESTAMP,
file_path TEXT NOT NULL,
event_count INTEGER DEFAULT 0,
size_bytes INTEGER DEFAULT 0,
metadata TEXT
)
''')
# Users table for authentication
conn.execute('''
CREATE TABLE IF NOT EXISTS users (
@@ -139,6 +185,7 @@ def init_db() -> None:
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
wifi_networks TEXT,
wifi_clients TEXT,
bt_devices TEXT,
rf_frequencies TEXT,
gps_coords TEXT,
@@ -146,6 +193,14 @@ def init_db() -> None:
)
''')
# Ensure new columns exist for older databases
try:
columns = {row['name'] for row in conn.execute("PRAGMA table_info(tscm_baselines)")}
if 'wifi_clients' not in columns:
conn.execute('ALTER TABLE tscm_baselines ADD COLUMN wifi_clients TEXT')
except Exception as e:
logger.debug(f"Schema update skipped for tscm_baselines: {e}")
# TSCM Sweeps - Individual sweep sessions
conn.execute('''
CREATE TABLE IF NOT EXISTS tscm_sweeps (
@@ -818,6 +873,7 @@ def create_tscm_baseline(
location: str | None = None,
description: str | None = None,
wifi_networks: list | None = None,
wifi_clients: list | None = None,
bt_devices: list | None = None,
rf_frequencies: list | None = None,
gps_coords: dict | None = None
@@ -831,13 +887,14 @@ def create_tscm_baseline(
with get_db() as conn:
cursor = conn.execute('''
INSERT INTO tscm_baselines
(name, location, description, wifi_networks, bt_devices, rf_frequencies, gps_coords)
VALUES (?, ?, ?, ?, ?, ?, ?)
(name, location, description, wifi_networks, wifi_clients, bt_devices, rf_frequencies, gps_coords)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
name,
location,
description,
json.dumps(wifi_networks) if wifi_networks else None,
json.dumps(wifi_clients) if wifi_clients else None,
json.dumps(bt_devices) if bt_devices else None,
json.dumps(rf_frequencies) if rf_frequencies else None,
json.dumps(gps_coords) if gps_coords else None