{"title":"Command Injection via subprocess.shell=True","language":"Python","severity":"Critical","cwe":"CWE-78","source_lines":[10,11],"flow_lines":[10,11,12,14],"sink_lines":[14],"vulnerable_code":"import subprocess\nimport logging\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\nlogger = logging.getLogger(__name__)\n\n@app.route('/api/iot/device/reboot', methods=['POST'])\ndef trigger_device_reboot():\n    device_mac = request.json.get('mac_address', '')\n    reboot_delay = request.json.get('delay_seconds', '0')\n    logger.info(f\"Initiating reboot for device: {device_mac}\")\n    wake_cmd = f\"wakeonlan {device_mac} && sleep {reboot_delay} && ssh root@{device_mac} 'reboot now'\"\n    try:\n        result = subprocess.run(wake_cmd, shell=True, capture_output=True, timeout=30, text=True)\n        return jsonify({\n            'status': 'success',\n            'device': device_mac,\n            'output': result.stdout\n        }), 200\n    except subprocess.TimeoutExpired:\n        return jsonify({'status': 'error', 'message': 'Reboot command timed out'}), 500\n    except Exception as e:\n        return jsonify({'status': 'error', 'message': str(e)}), 500","explanation":"The application takes unsanitized user input from request.json (device_mac and reboot_delay) and directly concatenates them into a shell command string that is executed via subprocess.run() with shell=True. This allows attackers to inject arbitrary shell commands through either parameter, leading to remote code execution on the server.","remediation":"The fix eliminates command injection by replacing the single shell=True call with separate subprocess.run() calls using argument lists (shell=False), which prevents shell interpretation of special characters. Additionally, strict input validation is applied: MAC addresses are validated against a regex pattern, and the delay parameter is validated as a bounded integer, ensuring no malicious payloads can be passed even to the argument-list based calls.","secure_code":"import subprocess\nimport logging\nimport re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\nlogger = logging.getLogger(__name__)\n\nMAC_ADDRESS_REGEX = re.compile(r'^([0-9A-Fa-f]{2}[:\\-]){5}([0-9A-Fa-f]{2})$')\n\ndef validate_mac_address(mac):\n    \"\"\"Validate that the input is a properly formatted MAC address.\"\"\"\n    if not mac or not MAC_ADDRESS_REGEX.match(mac):\n        return False\n    return True\n\ndef validate_delay(delay):\n    \"\"\"Validate that delay is a non-negative integer within acceptable range.\"\"\"\n    try:\n        delay_int = int(delay)\n        if delay_int < 0 or delay_int > 300:\n            return False\n        return True\n    except (ValueError, TypeError):\n        return False\n\n@app.route('/api/iot/device/reboot', methods=['POST'])\ndef trigger_device_reboot():\n    device_mac = request.json.get('mac_address', '')\n    reboot_delay = request.json.get('delay_seconds', '0')\n\n    if not validate_mac_address(device_mac):\n        return jsonify({'status': 'error', 'message': 'Invalid MAC address format'}), 400\n\n    if not validate_delay(reboot_delay):\n        return jsonify({'status': 'error', 'message': 'Invalid delay value. Must be an integer between 0 and 300.'}), 400\n\n    delay_seconds = str(int(reboot_delay))\n\n    logger.info(f\"Initiating reboot for device: {device_mac}\")\n\n    try:\n        # Step 1: Wake-on-LAN\n        wol_result = subprocess.run(\n            ['wakeonlan', device_mac],\n            capture_output=True, timeout=10, text=True\n        )\n        if wol_result.returncode != 0:\n            return jsonify({'status': 'error', 'message': 'Wake-on-LAN failed', 'output': wol_result.stderr}), 500\n\n        # Step 2: Sleep for the specified delay\n        if int(delay_seconds) > 0:\n            subprocess.run(\n                ['sleep', delay_seconds],\n                capture_output=True, timeout=int(delay_seconds) + 5\n            )\n\n        # Step 3: SSH reboot command\n        ssh_result = subprocess.run(\n            ['ssh', f'root@{device_mac}', 'reboot', 'now'],\n            capture_output=True, timeout=15, text=True\n        )\n\n        return jsonify({\n            'status': 'success',\n            'device': device_mac,\n            'output': wol_result.stdout + ssh_result.stdout\n        }), 200\n    except subprocess.TimeoutExpired:\n        return jsonify({'status': 'error', 'message': 'Reboot command timed out'}), 500\n    except Exception as e:\n        logger.error(f\"Reboot failed for {device_mac}: {str(e)}\")\n        return jsonify({'status': 'error', 'message': 'An internal error occurred'}), 500"}