mirror of
https://github.com/smittix/intercept.git
synced 2026-07-22 08:18:09 -07:00
Add security hardening and bias-t support
Security improvements: - Add interface name validation to prevent command injection - Fix XSS vulnerability in pager message display - Add security headers (X-Content-Type-Options, X-Frame-Options, etc.) - Disable Werkzeug debug PIN - Add security documentation Features: - Add bias-t power support for SDR dongles across all modes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,7 @@ Most features work with a basic RTL-SDR dongle (RTL2832U + R820T2).
|
|||||||
- [Usage Guide](docs/USAGE.md) - Detailed instructions for each mode
|
- [Usage Guide](docs/USAGE.md) - Detailed instructions for each mode
|
||||||
- [Hardware Guide](docs/HARDWARE.md) - SDR hardware and advanced setup
|
- [Hardware Guide](docs/HARDWARE.md) - SDR hardware and advanced setup
|
||||||
- [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issues and solutions
|
- [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issues and solutions
|
||||||
|
- [Security](docs/SECURITY.md) - Network security and best practices
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,30 @@ _app_start_time = _time.time()
|
|||||||
# Create Flask app
|
# Create Flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Disable Werkzeug debugger PIN (not needed for local development tool)
|
||||||
|
os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SECURITY HEADERS
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def add_security_headers(response):
|
||||||
|
"""Add security headers to all responses."""
|
||||||
|
# Prevent MIME type sniffing
|
||||||
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
# Prevent clickjacking
|
||||||
|
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||||
|
# Enable XSS filter
|
||||||
|
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||||
|
# Referrer policy
|
||||||
|
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||||
|
# Permissions policy (disable unnecessary features)
|
||||||
|
response.headers['Permissions-Policy'] = 'geolocation=(self), microphone=()'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# GLOBAL PROCESS MANAGEMENT
|
# GLOBAL PROCESS MANAGEMENT
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Security Considerations
|
||||||
|
|
||||||
|
INTERCEPT is designed as a **local signal intelligence tool** for personal use on trusted networks. This document outlines security considerations and best practices.
|
||||||
|
|
||||||
|
## Network Binding
|
||||||
|
|
||||||
|
By default, INTERCEPT binds to `0.0.0.0:5050`, making it accessible from any network interface. This is convenient for accessing the web UI from other devices on your local network, but has security implications:
|
||||||
|
|
||||||
|
### Recommendations
|
||||||
|
|
||||||
|
1. **Firewall Rules**: If you don't need remote access, configure your firewall to block external access to port 5050:
|
||||||
|
```bash
|
||||||
|
# Linux (iptables)
|
||||||
|
sudo iptables -A INPUT -p tcp --dport 5050 -s 127.0.0.1 -j ACCEPT
|
||||||
|
sudo iptables -A INPUT -p tcp --dport 5050 -j DROP
|
||||||
|
|
||||||
|
# macOS (pf)
|
||||||
|
echo "block in on en0 proto tcp from any to any port 5050" | sudo pfctl -ef -
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Bind to Localhost**: For local-only access, set the host environment variable:
|
||||||
|
```bash
|
||||||
|
export INTERCEPT_HOST=127.0.0.1
|
||||||
|
python intercept.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Trusted Networks Only**: Only run INTERCEPT on networks you trust. The application has no authentication mechanism.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
INTERCEPT does **not** include authentication. This is by design for ease of use as a personal tool. If you need to expose INTERCEPT to untrusted networks:
|
||||||
|
|
||||||
|
1. Use a reverse proxy (nginx, Caddy) with authentication
|
||||||
|
2. Use a VPN to access your home network
|
||||||
|
3. Use SSH port forwarding: `ssh -L 5050:localhost:5050 your-server`
|
||||||
|
|
||||||
|
## Security Headers
|
||||||
|
|
||||||
|
INTERCEPT includes the following security headers on all responses:
|
||||||
|
|
||||||
|
| Header | Value | Purpose |
|
||||||
|
|--------|-------|---------|
|
||||||
|
| `X-Content-Type-Options` | `nosniff` | Prevent MIME type sniffing |
|
||||||
|
| `X-Frame-Options` | `SAMEORIGIN` | Prevent clickjacking |
|
||||||
|
| `X-XSS-Protection` | `1; mode=block` | Enable browser XSS filter |
|
||||||
|
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Control referrer information |
|
||||||
|
| `Permissions-Policy` | `geolocation=(self), microphone=()` | Restrict browser features |
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
All user inputs are validated before use:
|
||||||
|
|
||||||
|
- **Network interface names**: Validated against strict regex pattern
|
||||||
|
- **Bluetooth interface names**: Must match `hciX` format
|
||||||
|
- **MAC addresses**: Validated format
|
||||||
|
- **Frequencies**: Validated range and format
|
||||||
|
- **File paths**: Protected against directory traversal
|
||||||
|
- **HTML output**: All user-provided content is escaped
|
||||||
|
|
||||||
|
## Subprocess Execution
|
||||||
|
|
||||||
|
INTERCEPT executes external tools (rtl_fm, airodump-ng, etc.) via subprocess. Security measures:
|
||||||
|
|
||||||
|
- **No shell execution**: All subprocess calls use list arguments, not shell strings
|
||||||
|
- **Input validation**: All user-provided arguments are validated before use
|
||||||
|
- **Process isolation**: Each tool runs in its own process with limited permissions
|
||||||
|
|
||||||
|
## Debug Mode
|
||||||
|
|
||||||
|
Debug mode is **disabled by default**. If enabled via `INTERCEPT_DEBUG=true`:
|
||||||
|
|
||||||
|
- The Werkzeug debugger PIN is disabled (not needed for local tool)
|
||||||
|
- Additional logging is enabled
|
||||||
|
- Stack traces are shown on errors
|
||||||
|
|
||||||
|
**Never run in debug mode on untrusted networks.**
|
||||||
|
|
||||||
|
## Reporting Security Issues
|
||||||
|
|
||||||
|
If you discover a security vulnerability, please report it by:
|
||||||
|
|
||||||
|
1. Opening a GitHub issue (for non-sensitive issues)
|
||||||
|
2. Emailing the maintainer directly (for sensitive issues)
|
||||||
|
|
||||||
|
Please include:
|
||||||
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce
|
||||||
|
- Potential impact
|
||||||
|
- Suggested fix (if any)
|
||||||
+3
-1
@@ -381,9 +381,11 @@ def start_adsb():
|
|||||||
builder = SDRFactory.get_builder(sdr_type)
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
|
|
||||||
# Build ADS-B decoder command
|
# Build ADS-B decoder command
|
||||||
|
bias_t = data.get('bias_t', False)
|
||||||
cmd = builder.build_adsb_command(
|
cmd = builder.build_adsb_command(
|
||||||
device=sdr_device,
|
device=sdr_device,
|
||||||
gain=float(gain)
|
gain=float(gain),
|
||||||
|
bias_t=bias_t
|
||||||
)
|
)
|
||||||
|
|
||||||
# For RTL-SDR, ensure we use the found dump1090 path
|
# For RTL-SDR, ensure we use the found dump1090 path
|
||||||
|
|||||||
+13
-2
@@ -21,6 +21,7 @@ import app as app_module
|
|||||||
from utils.dependencies import check_tool
|
from utils.dependencies import check_tool
|
||||||
from utils.logging import bluetooth_logger as logger
|
from utils.logging import bluetooth_logger as logger
|
||||||
from utils.sse import format_sse
|
from utils.sse import format_sse
|
||||||
|
from utils.validation import validate_bluetooth_interface
|
||||||
from data.oui import OUI_DATABASE, load_oui_database, get_manufacturer
|
from data.oui import OUI_DATABASE, load_oui_database, get_manufacturer
|
||||||
from data.patterns import AIRTAG_PREFIXES, TILE_PREFIXES, SAMSUNG_TRACKER
|
from data.patterns import AIRTAG_PREFIXES, TILE_PREFIXES, SAMSUNG_TRACKER
|
||||||
from utils.constants import (
|
from utils.constants import (
|
||||||
@@ -304,9 +305,14 @@ def start_bt_scan():
|
|||||||
|
|
||||||
data = request.json
|
data = request.json
|
||||||
scan_mode = data.get('mode', 'hcitool')
|
scan_mode = data.get('mode', 'hcitool')
|
||||||
interface = data.get('interface', 'hci0')
|
|
||||||
scan_ble = data.get('scan_ble', True)
|
scan_ble = data.get('scan_ble', True)
|
||||||
|
|
||||||
|
# Validate Bluetooth interface name
|
||||||
|
try:
|
||||||
|
interface = validate_bluetooth_interface(data.get('interface', 'hci0'))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
app_module.bt_interface = interface
|
app_module.bt_interface = interface
|
||||||
app_module.bt_devices = {}
|
app_module.bt_devices = {}
|
||||||
|
|
||||||
@@ -388,7 +394,12 @@ def stop_bt_scan():
|
|||||||
def reset_bt_adapter():
|
def reset_bt_adapter():
|
||||||
"""Reset Bluetooth adapter."""
|
"""Reset Bluetooth adapter."""
|
||||||
data = request.json
|
data = request.json
|
||||||
interface = data.get('interface', 'hci0')
|
|
||||||
|
# Validate Bluetooth interface name
|
||||||
|
try:
|
||||||
|
interface = validate_bluetooth_interface(data.get('interface', 'hci0'))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
with app_module.bt_lock:
|
with app_module.bt_lock:
|
||||||
if app_module.bt_process:
|
if app_module.bt_process:
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ scanner_config = {
|
|||||||
'scan_delay': 0.1, # Seconds between frequency hops (keep low for fast scanning)
|
'scan_delay': 0.1, # Seconds between frequency hops (keep low for fast scanning)
|
||||||
'device': 0,
|
'device': 0,
|
||||||
'gain': 40,
|
'gain': 40,
|
||||||
|
'bias_t': False, # Bias-T power for external LNA
|
||||||
}
|
}
|
||||||
|
|
||||||
# Activity log
|
# Activity log
|
||||||
@@ -177,6 +178,9 @@ def scanner_loop():
|
|||||||
'-g', str(scanner_config['gain']),
|
'-g', str(scanner_config['gain']),
|
||||||
'-d', str(scanner_config['device']),
|
'-d', str(scanner_config['device']),
|
||||||
]
|
]
|
||||||
|
# Add bias-t flag if enabled (for external LNA power)
|
||||||
|
if scanner_config.get('bias_t', False):
|
||||||
|
rtl_cmd.append('-T')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Start rtl_fm
|
# Start rtl_fm
|
||||||
@@ -365,6 +369,9 @@ def _start_audio_stream(frequency: float, modulation: str):
|
|||||||
'-d', str(scanner_config['device']),
|
'-d', str(scanner_config['device']),
|
||||||
'-l', str(scanner_config['squelch']),
|
'-l', str(scanner_config['squelch']),
|
||||||
]
|
]
|
||||||
|
# Add bias-t flag if enabled (for external LNA power)
|
||||||
|
if scanner_config.get('bias_t', False):
|
||||||
|
rtl_cmd.append('-T')
|
||||||
|
|
||||||
encoder_cmd = [
|
encoder_cmd = [
|
||||||
ffmpeg_path,
|
ffmpeg_path,
|
||||||
@@ -497,6 +504,7 @@ def start_scanner() -> Response:
|
|||||||
scanner_config['scan_delay'] = float(data.get('scan_delay', 0.5))
|
scanner_config['scan_delay'] = float(data.get('scan_delay', 0.5))
|
||||||
scanner_config['device'] = int(data.get('device', 0))
|
scanner_config['device'] = int(data.get('device', 0))
|
||||||
scanner_config['gain'] = int(data.get('gain', 40))
|
scanner_config['gain'] = int(data.get('gain', 40))
|
||||||
|
scanner_config['bias_t'] = bool(data.get('bias_t', False))
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
|
|||||||
+3
-1
@@ -233,6 +233,7 @@ def start_decoding() -> Response:
|
|||||||
builder = SDRFactory.get_builder(sdr_device.sdr_type)
|
builder = SDRFactory.get_builder(sdr_device.sdr_type)
|
||||||
|
|
||||||
# Build FM demodulation command
|
# Build FM demodulation command
|
||||||
|
bias_t = data.get('bias_t', False)
|
||||||
rtl_cmd = builder.build_fm_demod_command(
|
rtl_cmd = builder.build_fm_demod_command(
|
||||||
device=sdr_device,
|
device=sdr_device,
|
||||||
frequency_mhz=freq,
|
frequency_mhz=freq,
|
||||||
@@ -240,7 +241,8 @@ def start_decoding() -> Response:
|
|||||||
gain=float(gain) if gain and gain != '0' else None,
|
gain=float(gain) if gain and gain != '0' else None,
|
||||||
ppm=int(ppm) if ppm and ppm != '0' else None,
|
ppm=int(ppm) if ppm and ppm != '0' else None,
|
||||||
modulation='fm',
|
modulation='fm',
|
||||||
squelch=squelch if squelch and squelch != 0 else None
|
squelch=squelch if squelch and squelch != 0 else None,
|
||||||
|
bias_t=bias_t
|
||||||
)
|
)
|
||||||
|
|
||||||
multimon_cmd = ['multimon-ng', '-t', 'raw'] + decoders + ['-f', 'alpha', '-']
|
multimon_cmd = ['multimon-ng', '-t', 'raw'] + decoders + ['-f', 'alpha', '-']
|
||||||
|
|||||||
+3
-1
@@ -114,11 +114,13 @@ def start_sensor() -> Response:
|
|||||||
builder = SDRFactory.get_builder(sdr_device.sdr_type)
|
builder = SDRFactory.get_builder(sdr_device.sdr_type)
|
||||||
|
|
||||||
# Build ISM band decoder command
|
# Build ISM band decoder command
|
||||||
|
bias_t = data.get('bias_t', False)
|
||||||
cmd = builder.build_ism_command(
|
cmd = builder.build_ism_command(
|
||||||
device=sdr_device,
|
device=sdr_device,
|
||||||
frequency_mhz=freq,
|
frequency_mhz=freq,
|
||||||
gain=float(gain) if gain and gain != 0 else None,
|
gain=float(gain) if gain and gain != 0 else None,
|
||||||
ppm=int(ppm) if ppm and ppm != 0 else None
|
ppm=int(ppm) if ppm and ppm != 0 else None,
|
||||||
|
bias_t=bias_t
|
||||||
)
|
)
|
||||||
|
|
||||||
full_cmd = ' '.join(cmd)
|
full_cmd = ' '.join(cmd)
|
||||||
|
|||||||
+46
-8
@@ -19,7 +19,7 @@ import app as app_module
|
|||||||
from utils.dependencies import check_tool, get_tool_path
|
from utils.dependencies import check_tool, get_tool_path
|
||||||
from utils.logging import wifi_logger as logger
|
from utils.logging import wifi_logger as logger
|
||||||
from utils.process import is_valid_mac, is_valid_channel
|
from utils.process import is_valid_mac, is_valid_channel
|
||||||
from utils.validation import validate_wifi_channel, validate_mac_address
|
from utils.validation import validate_wifi_channel, validate_mac_address, validate_network_interface
|
||||||
from utils.sse import format_sse
|
from utils.sse import format_sse
|
||||||
from data.oui import get_manufacturer
|
from data.oui import get_manufacturer
|
||||||
from utils.constants import (
|
from utils.constants import (
|
||||||
@@ -303,11 +303,13 @@ def get_wifi_interfaces():
|
|||||||
def toggle_monitor_mode():
|
def toggle_monitor_mode():
|
||||||
"""Enable or disable monitor mode on an interface."""
|
"""Enable or disable monitor mode on an interface."""
|
||||||
data = request.json
|
data = request.json
|
||||||
interface = data.get('interface')
|
|
||||||
action = data.get('action', 'start')
|
action = data.get('action', 'start')
|
||||||
|
|
||||||
if not interface:
|
# Validate interface name to prevent command injection
|
||||||
return jsonify({'status': 'error', 'message': 'No interface specified'})
|
try:
|
||||||
|
interface = validate_network_interface(data.get('interface'))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
if action == 'start':
|
if action == 'start':
|
||||||
if check_tool('airmon-ng'):
|
if check_tool('airmon-ng'):
|
||||||
@@ -458,10 +460,19 @@ def start_wifi_scan():
|
|||||||
return jsonify({'status': 'error', 'message': 'Scan already running'})
|
return jsonify({'status': 'error', 'message': 'Scan already running'})
|
||||||
|
|
||||||
data = request.json
|
data = request.json
|
||||||
interface = data.get('interface') or app_module.wifi_monitor_interface
|
|
||||||
channel = data.get('channel')
|
channel = data.get('channel')
|
||||||
band = data.get('band', 'abg')
|
band = data.get('band', 'abg')
|
||||||
|
|
||||||
|
# Use provided interface or fall back to stored monitor interface
|
||||||
|
interface = data.get('interface')
|
||||||
|
if interface:
|
||||||
|
try:
|
||||||
|
interface = validate_network_interface(interface)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
else:
|
||||||
|
interface = app_module.wifi_monitor_interface
|
||||||
|
|
||||||
if not interface:
|
if not interface:
|
||||||
return jsonify({'status': 'error', 'message': 'No monitor interface available.'})
|
return jsonify({'status': 'error', 'message': 'No monitor interface available.'})
|
||||||
|
|
||||||
@@ -557,7 +568,16 @@ def send_deauth():
|
|||||||
target_bssid = data.get('bssid')
|
target_bssid = data.get('bssid')
|
||||||
target_client = data.get('client', 'FF:FF:FF:FF:FF:FF')
|
target_client = data.get('client', 'FF:FF:FF:FF:FF:FF')
|
||||||
count = data.get('count', 5)
|
count = data.get('count', 5)
|
||||||
interface = data.get('interface') or app_module.wifi_monitor_interface
|
|
||||||
|
# Validate interface
|
||||||
|
interface = data.get('interface')
|
||||||
|
if interface:
|
||||||
|
try:
|
||||||
|
interface = validate_network_interface(interface)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
else:
|
||||||
|
interface = app_module.wifi_monitor_interface
|
||||||
|
|
||||||
if not target_bssid:
|
if not target_bssid:
|
||||||
return jsonify({'status': 'error', 'message': 'Target BSSID required'})
|
return jsonify({'status': 'error', 'message': 'Target BSSID required'})
|
||||||
@@ -612,7 +632,16 @@ def capture_handshake():
|
|||||||
data = request.json
|
data = request.json
|
||||||
target_bssid = data.get('bssid')
|
target_bssid = data.get('bssid')
|
||||||
channel = data.get('channel')
|
channel = data.get('channel')
|
||||||
interface = data.get('interface') or app_module.wifi_monitor_interface
|
|
||||||
|
# Validate interface
|
||||||
|
interface = data.get('interface')
|
||||||
|
if interface:
|
||||||
|
try:
|
||||||
|
interface = validate_network_interface(interface)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
else:
|
||||||
|
interface = app_module.wifi_monitor_interface
|
||||||
|
|
||||||
if not target_bssid or not channel:
|
if not target_bssid or not channel:
|
||||||
return jsonify({'status': 'error', 'message': 'BSSID and channel required'})
|
return jsonify({'status': 'error', 'message': 'BSSID and channel required'})
|
||||||
@@ -701,7 +730,16 @@ def capture_pmkid():
|
|||||||
data = request.json
|
data = request.json
|
||||||
target_bssid = data.get('bssid')
|
target_bssid = data.get('bssid')
|
||||||
channel = data.get('channel')
|
channel = data.get('channel')
|
||||||
interface = data.get('interface') or app_module.wifi_monitor_interface
|
|
||||||
|
# Validate interface
|
||||||
|
interface = data.get('interface')
|
||||||
|
if interface:
|
||||||
|
try:
|
||||||
|
interface = validate_network_interface(interface)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
else:
|
||||||
|
interface = app_module.wifi_monitor_interface
|
||||||
|
|
||||||
if not target_bssid:
|
if not target_bssid:
|
||||||
return jsonify({'status': 'error', 'message': 'BSSID required'})
|
return jsonify({'status': 'error', 'message': 'BSSID required'})
|
||||||
|
|||||||
+42
-7
@@ -330,6 +330,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Global SDR Power Settings -->
|
||||||
|
<div class="section" style="padding: 8px; margin-bottom: 10px; background: rgba(0, 212, 255, 0.05); border-radius: 4px;">
|
||||||
|
<div class="checkbox-group" style="margin: 0;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="biasT" onchange="saveBiasTSetting()">
|
||||||
|
<span style="color: var(--accent-cyan);">Bias-T Power</span>
|
||||||
|
<span style="font-size: 10px; color: #666;">(LNA/Preamp)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toolStatusPager" class="info-text tool-status-section" style="display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
|
<div id="toolStatusPager" class="info-text tool-status-section" style="display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
|
||||||
<span>rtl_fm:</span><span class="tool-status {{ 'ok' if tools.rtl_fm else 'missing' }}">{{ 'OK' if tools.rtl_fm else 'Missing' }}</span>
|
<span>rtl_fm:</span><span class="tool-status {{ 'ok' if tools.rtl_fm else 'missing' }}">{{ 'OK' if tools.rtl_fm else 'Missing' }}</span>
|
||||||
<span>multimon-ng:</span><span class="tool-status {{ 'ok' if tools.multimon else 'missing' }}">{{ 'OK' if tools.multimon else 'Missing' }}</span>
|
<span>multimon-ng:</span><span class="tool-status {{ 'ok' if tools.multimon else 'missing' }}">{{ 'OK' if tools.multimon else 'Missing' }}</span>
|
||||||
@@ -2192,6 +2203,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load bias-T setting from localStorage
|
||||||
|
loadBiasTSetting();
|
||||||
|
|
||||||
// Initialize observer location input fields from saved location
|
// Initialize observer location input fields from saved location
|
||||||
const adsbLatInput = document.getElementById('adsbObsLat');
|
const adsbLatInput = document.getElementById('adsbObsLat');
|
||||||
const adsbLonInput = document.getElementById('adsbObsLon');
|
const adsbLonInput = document.getElementById('adsbObsLon');
|
||||||
@@ -2373,7 +2387,8 @@
|
|||||||
gain: gain,
|
gain: gain,
|
||||||
ppm: ppm,
|
ppm: ppm,
|
||||||
device: device,
|
device: device,
|
||||||
sdr_type: getSelectedSDRType()
|
sdr_type: getSelectedSDRType(),
|
||||||
|
bias_t: getBiasTEnabled()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add rtl_tcp params if using remote SDR
|
// Add rtl_tcp params if using remote SDR
|
||||||
@@ -2950,6 +2965,24 @@
|
|||||||
return document.getElementById('sdrTypeSelect').value;
|
return document.getElementById('sdrTypeSelect').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bias-T power setting
|
||||||
|
function saveBiasTSetting() {
|
||||||
|
const enabled = document.getElementById('biasT')?.checked || false;
|
||||||
|
localStorage.setItem('biasTEnabled', enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBiasTEnabled() {
|
||||||
|
return document.getElementById('biasT')?.checked || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBiasTSetting() {
|
||||||
|
const saved = localStorage.getItem('biasTEnabled');
|
||||||
|
if (saved === 'true') {
|
||||||
|
const checkbox = document.getElementById('biasT');
|
||||||
|
if (checkbox) checkbox.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleRemoteSDR() {
|
function toggleRemoteSDR() {
|
||||||
const useRemote = document.getElementById('useRemoteSDR').checked;
|
const useRemote = document.getElementById('useRemoteSDR').checked;
|
||||||
const configDiv = document.getElementById('remoteSDRConfig');
|
const configDiv = document.getElementById('remoteSDRConfig');
|
||||||
@@ -3017,7 +3050,8 @@
|
|||||||
ppm: ppm,
|
ppm: ppm,
|
||||||
device: device,
|
device: device,
|
||||||
sdr_type: getSelectedSDRType(),
|
sdr_type: getSelectedSDRType(),
|
||||||
protocols: protocols
|
protocols: protocols,
|
||||||
|
bias_t: getBiasTEnabled()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add rtl_tcp params if using remote SDR
|
// Add rtl_tcp params if using remote SDR
|
||||||
@@ -3206,10 +3240,10 @@
|
|||||||
msgEl.className = 'message ' + protoClass;
|
msgEl.className = 'message ' + protoClass;
|
||||||
msgEl.innerHTML = `
|
msgEl.innerHTML = `
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="protocol">${msg.protocol}</span>
|
<span class="protocol">${escapeHtml(msg.protocol)}</span>
|
||||||
<span class="msg-time" data-timestamp="${msg.timestamp}" title="${msg.timestamp}">${relativeTime}</span>
|
<span class="msg-time" data-timestamp="${escapeAttr(msg.timestamp)}" title="${escapeAttr(msg.timestamp)}">${escapeHtml(relativeTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="address">Address: ${msg.address}${msg.function ? ' | Func: ' + msg.function : ''}</div>
|
<div class="address">Address: ${escapeHtml(msg.address)}${msg.function ? ' | Func: ' + escapeHtml(msg.function) : ''}</div>
|
||||||
<div class="content ${isNumeric ? 'numeric' : ''}">${escapeHtml(msg.message)}</div>
|
<div class="content ${isNumeric ? 'numeric' : ''}">${escapeHtml(msg.message)}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -6802,7 +6836,7 @@
|
|||||||
fetch('/adsb/start', {
|
fetch('/adsb/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ gain, device, sdr_type })
|
body: JSON.stringify({ gain, device, sdr_type, bias_t: getBiasTEnabled() })
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -8135,7 +8169,8 @@
|
|||||||
modulation: modulation,
|
modulation: modulation,
|
||||||
squelch: squelch,
|
squelch: squelch,
|
||||||
dwell_time: dwell,
|
dwell_time: dwell,
|
||||||
device: device
|
device: device,
|
||||||
|
bias_t: getBiasTEnabled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
|
|||||||
@@ -195,3 +195,64 @@ def sanitize_device_name(name: str | None) -> str:
|
|||||||
return ''
|
return ''
|
||||||
# Escape HTML and limit length
|
# Escape HTML and limit length
|
||||||
return escape_html(str(name)[:64])
|
return escape_html(str(name)[:64])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_network_interface(name: Any) -> str:
|
||||||
|
"""
|
||||||
|
Validate network interface name to prevent command injection.
|
||||||
|
|
||||||
|
Interface names must:
|
||||||
|
- Start with a letter
|
||||||
|
- Contain only alphanumeric, underscore, or hyphen
|
||||||
|
- Be 1-15 characters long (Linux IFNAMSIZ limit)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Interface name to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validated interface name
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If interface name is invalid
|
||||||
|
"""
|
||||||
|
if not name or not isinstance(name, str):
|
||||||
|
raise ValueError("Interface name is required")
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
raise ValueError("Interface name cannot be empty")
|
||||||
|
|
||||||
|
if len(name) > 15:
|
||||||
|
raise ValueError(f"Interface name too long (max 15 chars): {name}")
|
||||||
|
|
||||||
|
# Must start with letter, contain only alphanumeric/underscore/hyphen
|
||||||
|
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', name):
|
||||||
|
raise ValueError(f"Invalid interface name: {name}")
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bluetooth_interface(name: Any) -> str:
|
||||||
|
"""
|
||||||
|
Validate Bluetooth interface name (hciX format).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Interface name to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validated interface name
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If interface name is invalid
|
||||||
|
"""
|
||||||
|
if not name or not isinstance(name, str):
|
||||||
|
raise ValueError("Bluetooth interface name is required")
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
|
||||||
|
# Must be hciX format where X is a number 0-255
|
||||||
|
if not re.match(r'^hci([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', name):
|
||||||
|
raise ValueError(f"Invalid Bluetooth interface name (expected hciX): {name}")
|
||||||
|
|
||||||
|
return name
|
||||||
|
|||||||
Reference in New Issue
Block a user