{"title":"Command Injection via subprocess.run(shell=True)","language":"Python","severity":"Critical","cwe":"CWE-78","source_lines":[3],"flow_lines":[3,4,5,3,7,8],"sink_lines":[5,8],"vulnerable_code":"import subprocess\n\ndef provision_iot_device(device_mac, firmware_url):\n    sanitized_mac = device_mac.replace(':', '')\n    flash_cmd = f\"curl -s {firmware_url} | dd of=/dev/iot_{sanitized_mac} bs=4M\"\n    result = subprocess.run(flash_cmd, shell=True, capture_output=True, text=True)\n    if result.returncode == 0:\n        reboot_sequence = f\"echo 'REBOOT' > /sys/devices/iot/{sanitized_mac}/control\"\n        subprocess.run(reboot_sequence, shell=True)\n        return {\"status\": \"provisioned\", \"device\": sanitized_mac}\n    return {\"status\": \"failed\", \"error\": result.stderr}","explanation":"The function accepts untrusted user input via firmware_url and device_mac parameters and passes them directly into shell commands executed via subprocess.run(shell=True). While device_mac receives minimal sanitization (colon removal), the firmware_url parameter is completely unsanitized, allowing attackers to inject arbitrary shell commands through command separators, pipes, or substitution operators.","remediation":"The fix eliminates shell=True by using subprocess with argument lists, separating the curl and dd commands into individual subprocess calls piped through Python. Input validation is added for both the MAC address (strict regex) and firmware URL (scheme validation and character whitelist). The reboot command is replaced with a direct file write operation instead of a shell echo redirect.","secure_code":"import subprocess\nimport re\nfrom urllib.parse import urlparse\n\n\ndef validate_mac_address(device_mac):\n    \"\"\"Validate MAC address format strictly.\"\"\"\n    pattern = r'^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'\n    if not re.match(pattern, device_mac):\n        raise ValueError(f\"Invalid MAC address format: {device_mac}\")\n    return device_mac.replace(':', '')\n\n\ndef validate_firmware_url(firmware_url):\n    \"\"\"Validate firmware URL to ensure it's a proper HTTP(S) URL.\"\"\"\n    parsed = urlparse(firmware_url)\n    if parsed.scheme not in ('http', 'https'):\n        raise ValueError(f\"Invalid URL scheme: {parsed.scheme}. Only http and https are allowed.\")\n    if not parsed.hostname:\n        raise ValueError(\"Invalid URL: no hostname found.\")\n    allowed_url_pattern = r'^https?://[A-Za-z0-9._~:/?#\\[\\]@!$&\\'()*+,;=%-]+$'\n    if not re.match(allowed_url_pattern, firmware_url):\n        raise ValueError(f\"URL contains disallowed characters: {firmware_url}\")\n    return firmware_url\n\n\ndef provision_iot_device(device_mac, firmware_url):\n    \"\"\"Provision an IoT device by flashing firmware.\"\"\"\n    sanitized_mac = validate_mac_address(device_mac)\n    validated_url = validate_firmware_url(firmware_url)\n\n    device_path = f\"/dev/iot_{sanitized_mac}\"\n    control_path = f\"/sys/devices/iot/{sanitized_mac}/control\"\n\n    curl_process = subprocess.run(\n        [\"curl\", \"-s\", \"--max-redirs\", \"3\", validated_url],\n        capture_output=True\n    )\n\n    if curl_process.returncode != 0:\n        return {\"status\": \"failed\", \"error\": \"Failed to download firmware\"}\n\n    dd_process = subprocess.run(\n        [\"dd\", f\"of={device_path}\", \"bs=4M\"],\n        input=curl_process.stdout,\n        capture_output=True\n    )\n\n    if dd_process.returncode == 0:\n        try:\n            with open(control_path, 'w') as control_file:\n                control_file.write('REBOOT')\n        except IOError as e:\n            return {\"status\": \"failed\", \"error\": f\"Failed to send reboot command: {e}\"}\n        return {\"status\": \"provisioned\", \"device\": sanitized_mac}\n\n    return {\"status\": \"failed\", \"error\": dd_process.stderr.decode('utf-8', errors='replace')}"}