diff --git a/scripts/recovery/autonuke.py b/scripts/recovery/autonuke.py index e53c30a..42e7182 100755 --- a/scripts/recovery/autonuke.py +++ b/scripts/recovery/autonuke.py @@ -21,6 +21,7 @@ import sys import json import subprocess +import shlex import time from pathlib import Path from typing import Dict, List, Tuple, Optional @@ -113,17 +114,37 @@ def confirm_action(self, message: str, danger_level: str = "LOW") -> bool: response = input(f"{Colors.CYAN}Continue? [y/N]: {Colors.END}").strip().lower() return response in ['y', 'yes'] - def run_command(self, cmd: str, shell: bool = True) -> Tuple[int, str, str]: - """Run a command and return exit code, stdout, stderr""" + def run_command(self, cmd: str, shell: bool = False) -> Tuple[int, str, str]: + """Run a command and return exit code, stdout, stderr + + Args: + cmd: Command string (will be split into list if shell=False) + shell: If True, runs command in shell (use only for trusted input) + + Note: For security, shell=False is default. Command is split using shlex.split() + which properly handles quoted arguments and special characters. + """ self.log(f"Executing: {cmd}") try: - result = subprocess.run( - cmd, - shell=shell, - capture_output=True, - text=True, - cwd=self.project_root - ) + if shell: + # Only use shell mode for trusted, hardcoded commands + result = subprocess.run( + cmd, + shell=True, + capture_output=True, + text=True, + cwd=self.project_root + ) + else: + # Safer mode: split command using shlex for proper quoting + cmd_list = shlex.split(cmd) + result = subprocess.run( + cmd_list, + shell=False, + capture_output=True, + text=True, + cwd=self.project_root + ) return result.returncode, result.stdout, result.stderr except Exception as e: self.log(f"Command failed: {e}", "ERROR") diff --git a/scripts/recovery/hardware-recovery.sh b/scripts/recovery/hardware-recovery.sh index dc8dbbe..f336334 100755 --- a/scripts/recovery/hardware-recovery.sh +++ b/scripts/recovery/hardware-recovery.sh @@ -82,6 +82,7 @@ if [ -f "$FIRMWARE_IMAGE" ]; then [ -n "$VERBOSE" ] && ARGS="$ARGS -v" [ -n "$VERIFY_ONLY" ] && ARGS="$ARGS --verify-only" + # shellcheck disable=SC2086 sudo python3 scripts/hardware_firmware_recovery.py $ARGS --output hardware_recovery_results.json else echo "ERROR: Clean firmware image not found at $FIRMWARE_IMAGE" diff --git a/scripts/recovery/nuclear-wipe.sh b/scripts/recovery/nuclear-wipe.sh index 9f5f796..f9d1018 100755 --- a/scripts/recovery/nuclear-wipe.sh +++ b/scripts/recovery/nuclear-wipe.sh @@ -18,6 +18,17 @@ if [ "$EUID" -ne 0 ]; then exit 1 fi +# Validate device path format for security +validate_device_path() { + local device="$1" + if [[ ! "$device" =~ ^/dev/(sd[a-z]{1,4}|nvme[0-9]+n[0-9]+|vd[a-z]{1,2}|mmcblk[0-9]+)$ ]]; then + echo "❌ Invalid device path format: $device" + echo " Expected format: /dev/sdX, /dev/nvmeXnY, /dev/vdX, or /dev/mmcblkX" + return 1 + fi + return 0 +} + # Check if nwipe is installed if ! command -v nwipe &> /dev/null; then echo "⚠️ nwipe not found - attempting to install..." @@ -94,6 +105,11 @@ case "$choice" in exit 1 fi + # Validate device path format for security + if ! validate_device_path "$device"; then + exit 1 + fi + echo "⚠️ FINAL CONFIRMATION" echo " Device: $device" echo " Method: Quick wipe (zeros)" @@ -115,6 +131,11 @@ case "$choice" in exit 1 fi + # Validate device path format for security + if ! validate_device_path "$device"; then + exit 1 + fi + echo "⚠️ FINAL CONFIRMATION" echo " Device: $device" echo " Method: DoD Short (3 passes)" @@ -137,6 +158,11 @@ case "$choice" in exit 1 fi + # Validate device path format for security + if ! validate_device_path "$device"; then + exit 1 + fi + echo "⚠️ FINAL CONFIRMATION" echo " Device: $device" echo " Method: PRNG Stream (cryptographically secure)" diff --git a/scripts/secure-boot/enable-secureboot-kexec.sh b/scripts/secure-boot/enable-secureboot-kexec.sh index 6cb77e6..f7013a8 100755 --- a/scripts/secure-boot/enable-secureboot-kexec.sh +++ b/scripts/secure-boot/enable-secureboot-kexec.sh @@ -54,13 +54,14 @@ fi # Check if Secure Boot is already enabled check_secureboot_enabled() { - if [ -f /sys/firmware/efi/efivars/SecureBoot-* ]; then - local sb_file=$(ls /sys/firmware/efi/efivars/SecureBoot-* 2>/dev/null | head -1) - if [ -n "$sb_file" ]; then - local sb_status=$(od -An -t u1 -j 4 -N 1 "$sb_file" 2>/dev/null | tr -d ' ') + local sb_file + for sb_file in /sys/firmware/efi/efivars/SecureBoot-*; do + if [ -f "$sb_file" ]; then + local sb_status + sb_status=$(od -An -t u1 -j 4 -N 1 "$sb_file" 2>/dev/null | tr -d ' ') [ "$sb_status" = "1" ] && return 0 fi - fi + done return 1 } @@ -220,7 +221,7 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then fi # Create a script that will run after first kexec -TEMP_SCRIPT="/tmp/phoenixboot_secureboot_enable_phase2.sh" +TEMP_SCRIPT=$(mktemp /tmp/phoenixboot_secureboot_enable_phase2.XXXXXX.sh) cat > "$TEMP_SCRIPT" << 'EOF' #!/bin/bash diff --git a/scripts/secure-boot/keys-centralize.sh b/scripts/secure-boot/keys-centralize.sh index 017c25f..52f7f01 100755 --- a/scripts/secure-boot/keys-centralize.sh +++ b/scripts/secure-boot/keys-centralize.sh @@ -23,7 +23,7 @@ DRY_RUN=${DRY_RUN:-0} PRUNE=${PRUNE:-0} if [ "${1:-}" = "--prune" ]; then PRUNE=1; fi -run() { if [ "$DRY_RUN" = 1 ]; then echo "DRY: $*"; else eval "$*"; fi } +run() { if [ "$DRY_RUN" = 1 ]; then echo "DRY: $*"; else "$@"; fi } move_if_exists() { local src="$1" dest_dir="$2" diff --git a/scripts/uefi-tools/uuefi-apply.sh b/scripts/uefi-tools/uuefi-apply.sh index ddef444..e2febcc 100755 --- a/scripts/uefi-tools/uuefi-apply.sh +++ b/scripts/uefi-tools/uuefi-apply.sh @@ -7,7 +7,7 @@ info "☠ UUEFI apply (set BootNext for selected app)" # Dry-run mode: UUEFI_DRYRUN=1 DRY=${UUEFI_DRYRUN:-} -run() { if [ -n "$DRY" ]; then echo "DRYRUN: $*"; else eval "$*"; fi } +run() { if [ -n "$DRY" ]; then echo "DRYRUN: $*"; else "$@"; fi } need_sudo() { if [ -n "$DRY" ]; then echo sudo -n "$@" || true; else sudo -n "$@"; fi } diff --git a/scripts/validation/detect_bootkit.py b/scripts/validation/detect_bootkit.py index 07071b0..1e1b46b 100755 --- a/scripts/validation/detect_bootkit.py +++ b/scripts/validation/detect_bootkit.py @@ -372,7 +372,7 @@ def main(): time.sleep(10) # Trigger recovery - os.system("sudo make reboot-to-vm") + subprocess.run(["sudo", "make", "reboot-to-vm"], check=False) return 0 diff --git a/web/hardware_database_server.py b/web/hardware_database_server.py index 03e9975..59c4c35 100644 --- a/web/hardware_database_server.py +++ b/web/hardware_database_server.py @@ -17,14 +17,16 @@ from flask import Flask, request, jsonify, render_template_string, send_file import json import os +import secrets from datetime import datetime from pathlib import Path from typing import Dict, List import sqlite3 import hashlib +import re app = Flask(__name__) -app.config['SECRET_KEY'] = 'phoenix_guard_hardware_db' +app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', secrets.token_hex(32)) # Database setup DB_PATH = Path("hardware_profiles.db") @@ -512,8 +514,20 @@ def search_hardware(): @app.route('/api/download/') def download_config(hardware_id): """Download universal BIOS config for hardware""" + # Validate hardware_id to prevent path traversal + if not re.match(r'^[a-zA-Z0-9_-]+$', hardware_id): + return "Invalid hardware ID format", 400 + profile_file = UPLOADS_PATH / f"{hardware_id}.json" + # Resolve symlinks and verify path stays within UPLOADS_PATH + try: + profile_file = profile_file.resolve() + if not str(profile_file).startswith(str(UPLOADS_PATH.resolve())): + return "Invalid file path", 400 + except (OSError, RuntimeError): + return "Invalid file path", 400 + if not profile_file.exists(): return "Profile not found", 404