{"title":"Command Injection via subprocess.run(shell=True)","language":"Python","severity":"Critical","cwe":"CWE-78","source_lines":[2,3],"flow_lines":[2,5,6,3,8,9],"sink_lines":[6,9],"vulnerable_code":"import subprocess\n\ndef provision_iot_device(device_mac, firmware_url):\n    sanitized_mac = device_mac.replace(':', '')\n    cmd = f\"curl -s {firmware_url} | sha256sum && echo 'Provisioning device {sanitized_mac}'\"\n    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)\n    if result.returncode == 0:\n        log_entry = f\"iot-provision --mac={device_mac} --status=success\"\n        subprocess.run(log_entry, shell=True)\n        return {\"status\": \"provisioned\", \"device\": device_mac, \"hash\": result.stdout.strip()}\n    return {\"status\": \"failed\", \"device\": device_mac}","explanation":"The function accepts unsanitized user input (device_mac and firmware_url) and directly interpolates them into shell commands executed via subprocess.run() with shell=True. While device_mac receives minimal sanitization (colon removal), both parameters can contain shell metacharacters that enable command injection. The firmware_url is completely unsanitized and directly embedded in a piped command sequence, and device_mac is used unsanitized in the logging subprocess call.","remediation":"The fix eliminates all shell=True subprocess calls by: (1) replacing curl with Python's urllib for downloading firmware, (2) computing SHA256 using Python's hashlib instead of piping to sha256sum, (3) using subprocess with argument lists (no shell) for the external logging tool, and (4) adding strict input validation with regex for MAC addresses and URL scheme whitelisting.","secure_code":"import subprocess\nimport re\nimport hashlib\nimport logging\nimport urllib.request\nimport urllib.parse\n\nlogger = logging.getLogger(__name__)\n\nMAC_PATTERN = re.compile(r'^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$')\nALLOWED_SCHEMES = {'http', 'https'}\n\ndef validate_mac_address(device_mac):\n    if not MAC_PATTERN.match(device_mac):\n        raise ValueError(f\"Invalid MAC address format: {device_mac}\")\n    return device_mac\n\ndef validate_firmware_url(firmware_url):\n    parsed = urllib.parse.urlparse(firmware_url)\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        raise ValueError(f\"Invalid URL scheme: {parsed.scheme}. Only HTTP/HTTPS allowed.\")\n    if not parsed.hostname:\n        raise ValueError(\"Invalid URL: no hostname found.\")\n    return firmware_url\n\ndef provision_iot_device(device_mac, firmware_url):\n    device_mac = validate_mac_address(device_mac)\n    firmware_url = validate_firmware_url(firmware_url)\n\n    sanitized_mac = device_mac.replace(':', '')\n\n    try:\n        req = urllib.request.Request(firmware_url)\n        with urllib.request.urlopen(req, timeout=30) as response:\n            firmware_data = response.read()\n    except Exception as e:\n        logger.error(f\"Failed to download firmware: {e}\")\n        return {\"status\": \"failed\", \"device\": device_mac}\n\n    firmware_hash = hashlib.sha256(firmware_data).hexdigest()\n\n    logger.info(f\"iot-provision mac={sanitized_mac} status=success hash={firmware_hash}\")\n\n    try:\n        subprocess.run(\n            [\"iot-provision\", \"--mac\", device_mac, \"--status\", \"success\"],\n            capture_output=True,\n            text=True,\n            check=False\n        )\n    except FileNotFoundError:\n        logger.warning(\"iot-provision binary not found, skipping external logging\")\n\n    return {\"status\": \"provisioned\", \"device\": device_mac, \"hash\": firmware_hash}"}