{"title":"Command Injection via subprocess.shell","language":"Python","severity":"Critical","cwe":"CWE-78","source_lines":[3],"flow_lines":[3,4,5,3,4,8],"sink_lines":[5,8],"vulnerable_code":"import subprocess\n\ndef rotate_iot_device_logs(device_id, compression_level):\n    log_path = f\"/var/iot/devices/{device_id}/logs\"\n    archive_cmd = f\"tar -czf /backup/{device_id}_logs.tar.gz --level={compression_level} {log_path}\"\n    result = subprocess.run(archive_cmd, shell=True, capture_output=True, text=True)\n    if result.returncode == 0:\n        cleanup_cmd = f\"find {log_path} -type f -mtime +7 -delete\"\n        subprocess.run(cleanup_cmd, shell=True)\n        return {\"status\": \"success\", \"archive\": f\"{device_id}_logs.tar.gz\"}\n    return {\"status\": \"failed\", \"error\": result.stderr}","explanation":"The function accepts untrusted user input (device_id and compression_level) and directly interpolates them into shell commands without sanitization. Using subprocess.run() with shell=True enables command injection, allowing attackers to execute arbitrary commands by injecting shell metacharacters in either parameter.","remediation":"The fix eliminates command injection by replacing shell=True with argument lists (shell=False by default), validating device_id with a strict whitelist regex, validating compression_level as an integer within expected range, and using os.path.realpath() to prevent path traversal attacks.","secure_code":"import subprocess\nimport re\nimport os\n\ndef rotate_iot_device_logs(device_id, compression_level):\n    # Validate device_id: only allow alphanumeric characters, hyphens, and underscores\n    if not re.match(r'^[a-zA-Z0-9_\\-]+$', device_id):\n        return {\"status\": \"failed\", \"error\": \"Invalid device_id format\"}\n    \n    # Validate compression_level: must be an integer between 1 and 9\n    try:\n        compression_level = int(compression_level)\n        if compression_level < 1 or compression_level > 9:\n            raise ValueError(\"Compression level out of range\")\n    except (ValueError, TypeError):\n        return {\"status\": \"failed\", \"error\": \"Invalid compression level. Must be integer 1-9.\"}\n    \n    log_path = f\"/var/iot/devices/{device_id}/logs\"\n    archive_path = f\"/backup/{device_id}_logs.tar.gz\"\n    \n    # Verify the log path exists and is within expected directory\n    real_log_path = os.path.realpath(log_path)\n    if not real_log_path.startswith(\"/var/iot/devices/\"):\n        return {\"status\": \"failed\", \"error\": \"Invalid log path\"}\n    \n    # Use subprocess with argument list (shell=False) to prevent command injection\n    archive_cmd = [\n        \"tar\", \"-czf\", archive_path,\n        f\"--level={compression_level}\", real_log_path\n    ]\n    result = subprocess.run(archive_cmd, capture_output=True, text=True)\n    \n    if result.returncode == 0:\n        cleanup_cmd = [\n            \"find\", real_log_path, \"-type\", \"f\", \"-mtime\", \"+7\", \"-delete\"\n        ]\n        subprocess.run(cleanup_cmd)\n        return {\"status\": \"success\", \"archive\": f\"{device_id}_logs.tar.gz\"}\n    return {\"status\": \"failed\", \"error\": result.stderr}"}