From bb6386783aa8b02a62a5894ea42281d0093353e1 Mon Sep 17 00:00:00 2001 From: James Smith Date: Sun, 21 Dec 2025 14:11:00 +0000 Subject: [PATCH] Security fixes and Bluetooth improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security: - Add escapeAttr() and validation functions to prevent XSS in onclick handlers - Escape BSSID, MAC, channel, manufacturer, tracker names in HTML output - Add backend is_valid_mac() and is_valid_channel() validation - Validate MAC addresses in /wifi/deauth, /wifi/handshake/capture, /bt/ping, /bt/dos - Add bounds checking for count/size parameters to prevent abuse Bluetooth: - Fix stuck scan state by checking if process is actually running - Add /bt/reset endpoint to force reset adapter and clear state - Add "Reset Adapter" button to UI WiFi: - Improve monitor interface regex to require digits (prevents matching "airmon") - Add second fallback to look for exact interface name + "mon" Other: - Fix redundant CSV parsing bounds check - Remove debug logging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- intercept.py | 204 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 187 insertions(+), 17 deletions(-) diff --git a/intercept.py b/intercept.py index 3aa060d..797c21e 100755 --- a/intercept.py +++ b/intercept.py @@ -1692,6 +1692,9 @@ HTML_TEMPLATE = ''' + - + + `; @@ -3740,6 +3763,29 @@ HTML_TEMPLATE = ''' }); } + function resetBtAdapter() { + const iface = document.getElementById('btInterfaceSelect')?.value || 'hci0'; + fetch('/bt/reset', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({interface: iface}) + }).then(r => r.json()) + .then(data => { + setBtRunning(false); + if (btEventSource) { + btEventSource.close(); + btEventSource = null; + } + if (data.status === 'success') { + showInfo('Bluetooth adapter reset. Status: ' + (data.is_up ? 'UP' : 'DOWN')); + // Refresh interface list + if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces(); + } else { + showError('Reset failed: ' + data.message); + } + }); + } + function setBtRunning(running) { isBtRunning = running; document.getElementById('statusDot').classList.toggle('running', running); @@ -3838,26 +3884,26 @@ HTML_TEMPLATE = ''' card.innerHTML = `
${typeIcon} ${escapeHtml(device.name)} - ${device.type.toUpperCase()} + ${escapeHtml(device.type.toUpperCase())}
MAC
-
${device.mac}
+
${escapeHtml(device.mac)}
Manufacturer
-
${device.manufacturer}
+
${escapeHtml(device.manufacturer)}
${device.tracker ? `
Tracker
-
${device.tracker.name}
+
${escapeHtml(device.tracker.name)}
` : ''}
- - + +
`; @@ -3873,8 +3919,8 @@ HTML_TEMPLATE = ''' const alert = document.createElement('div'); alert.style.cssText = 'padding: 8px; margin-bottom: 5px; background: rgba(255,51,102,0.1); border-left: 2px solid var(--accent-red); font-family: JetBrains Mono, monospace;'; alert.innerHTML = ` -
âš  ${device.tracker.name} Detected
-
${device.mac}
+
âš  ${escapeHtml(device.tracker.name)} Detected
+
${escapeHtml(device.mac)}
`; list.insertBefore(alert, list.firstChild); } @@ -4153,6 +4199,23 @@ def check_tool(name): return shutil.which(name) is not None +def is_valid_mac(mac): + """Validate MAC address format.""" + import re + if not mac: + return False + return bool(re.match(r'^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$', mac)) + + +def is_valid_channel(channel): + """Validate WiFi channel number.""" + try: + ch = int(channel) + return 1 <= ch <= 200 + except (ValueError, TypeError): + return False + + def detect_devices(): """Detect RTL-SDR devices.""" devices = [] @@ -4861,8 +4924,12 @@ def toggle_monitor_mode(): # Look for "on mon" pattern first (most reliable) match = re.search(r'\bon\s+(\w+mon)\b', output, re.IGNORECASE) if not match: - # Fallback: look for interface name ending in 'mon' - match = re.search(r'\b(\w+mon)\b', output) + # Fallback: look for interface pattern like wlan0mon, wlp3s0mon (must have a digit) + match = re.search(r'\b(\w*\d+\w*mon)\b', output) + if not match: + # Second fallback: look for the original interface + mon in output + iface_pattern = re.escape(interface) + r'mon' + match = re.search(r'\b(' + iface_pattern + r')\b', output) if match: wifi_monitor_interface = match.group(1) else: @@ -4949,7 +5016,7 @@ def parse_airodump_csv(csv_path): 'beacons': parts[9], 'ivs': parts[10], 'lan_ip': parts[11], - 'essid': parts[13] if len(parts) > 13 else 'Hidden' + 'essid': parts[13] or 'Hidden' } elif 'Station MAC' in header: @@ -5043,7 +5110,8 @@ def stream_airodump_output(process, csv_path): wifi_networks = networks wifi_clients = clients last_parse = current_time - elif current_time - start_time > 5 and not csv_found: + + if current_time - start_time > 5 and not csv_found: # No CSV after 5 seconds - likely a problem wifi_queue.put({'type': 'error', 'text': 'No scan data after 5 seconds. Check if monitor mode is properly enabled.'}) start_time = current_time + 30 # Don't spam this message @@ -5200,6 +5268,21 @@ def send_deauth(): if not target_bssid: return jsonify({'status': 'error', 'message': 'Target BSSID required'}) + # Validate MAC addresses to prevent command injection + if not is_valid_mac(target_bssid): + return jsonify({'status': 'error', 'message': 'Invalid BSSID format'}) + + if not is_valid_mac(target_client): + return jsonify({'status': 'error', 'message': 'Invalid client MAC format'}) + + # Validate count to prevent abuse + try: + count = int(count) + if count < 1 or count > 100: + count = 5 + except (ValueError, TypeError): + count = 5 + if not interface: return jsonify({'status': 'error', 'message': 'No monitor interface'}) @@ -5244,10 +5327,18 @@ def capture_handshake(): if not target_bssid or not channel: return jsonify({'status': 'error', 'message': 'BSSID and channel required'}) + # Validate inputs to prevent command injection + if not is_valid_mac(target_bssid): + return jsonify({'status': 'error', 'message': 'Invalid BSSID format'}) + + if not is_valid_channel(channel): + return jsonify({'status': 'error', 'message': 'Invalid channel'}) + with wifi_lock: if wifi_process: return jsonify({'status': 'error', 'message': 'Scan already running. Stop it first.'}) + # Safe to use in path after validation capture_path = f'/tmp/intercept_handshake_{target_bssid.replace(":", "")}' cmd = [ @@ -5651,8 +5742,14 @@ def start_bt_scan(): global bt_process, bt_devices, bt_interface with bt_lock: + # Check if process is actually still running (not just set) if bt_process: - return jsonify({'status': 'error', 'message': 'Scan already running'}) + if bt_process.poll() is None: + # Process is actually running + return jsonify({'status': 'error', 'message': 'Scan already running'}) + else: + # Process died, clear the state + bt_process = None data = request.json scan_mode = data.get('mode', 'hcitool') @@ -5771,6 +5868,48 @@ def stop_bt_scan(): return jsonify({'status': 'not_running'}) +@app.route('/bt/reset', methods=['POST']) +def reset_bt_adapter(): + """Reset Bluetooth adapter and clear scan state.""" + global bt_process + + data = request.json + interface = data.get('interface', 'hci0') + + with bt_lock: + # Force clear the process state + if bt_process: + try: + bt_process.terminate() + bt_process.wait(timeout=2) + except: + try: + bt_process.kill() + except: + pass + bt_process = None + + # Reset the adapter + try: + subprocess.run(['hciconfig', interface, 'down'], capture_output=True, timeout=5) + subprocess.run(['hciconfig', interface, 'up'], capture_output=True, timeout=5) + + # Check if adapter is up + result = subprocess.run(['hciconfig', interface], capture_output=True, text=True, timeout=5) + is_up = 'UP RUNNING' in result.stdout + + bt_queue.put({'type': 'info', 'text': f'Bluetooth adapter {interface} reset'}) + + return jsonify({ + 'status': 'success', + 'message': f'Adapter {interface} reset', + 'is_up': is_up + }) + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + @app.route('/bt/enum', methods=['POST']) def enum_bt_services(): """Enumerate services on a Bluetooth device.""" @@ -5832,6 +5971,18 @@ def ping_bt_device(): if not target_mac: return jsonify({'status': 'error', 'message': 'Target MAC required'}) + # Validate MAC address + if not is_valid_mac(target_mac): + return jsonify({'status': 'error', 'message': 'Invalid MAC address format'}) + + # Validate count + try: + count = int(count) + if count < 1 or count > 50: + count = 5 + except (ValueError, TypeError): + count = 5 + try: result = subprocess.run( ['l2ping', '-c', str(count), target_mac], @@ -5863,6 +6014,25 @@ def dos_bt_device(): if not target_mac: return jsonify({'status': 'error', 'message': 'Target MAC required'}) + # Validate MAC address + if not is_valid_mac(target_mac): + return jsonify({'status': 'error', 'message': 'Invalid MAC address format'}) + + # Validate count and size to prevent abuse + try: + count = int(count) + if count < 1 or count > 1000: + count = 100 + except (ValueError, TypeError): + count = 100 + + try: + size = int(size) + if size < 1 or size > 1500: + size = 600 + except (ValueError, TypeError): + size = 600 + try: # l2ping flood with large packets result = subprocess.run(