{"title":"Command Injection via subprocess.shell=True","language":"Python","severity":"Critical","cwe":"CWE-78","source_lines":[3],"flow_lines":[3,6,7,8,11],"sink_lines":[11],"vulnerable_code":"import subprocess\nimport logging\n\ndef provision_iot_device(device_mac, firmware_url, config_params):\n    logger = logging.getLogger('iot_provisioner')\n    logger.info(f'Provisioning device: {device_mac}')\n    flash_cmd = f\"curl -s {firmware_url} | dd of=/dev/mtdblock0 bs=1M && \"\n    flash_cmd += f\"echo '{config_params}' > /etc/iot/device.conf && \"\n    flash_cmd += f\"ifconfig eth0 hw ether {device_mac}\"\n    try:\n        result = subprocess.run(flash_cmd, shell=True, capture_output=True, timeout=120)\n        if result.returncode == 0:\n            logger.info(f'Device {device_mac} provisioned successfully')\n            return {'status': 'success', 'device': device_mac, 'output': result.stdout.decode()}\n        else:\n            logger.error(f'Provisioning failed: {result.stderr.decode()}')\n            return {'status': 'error', 'message': result.stderr.decode()}\n    except subprocess.TimeoutExpired:\n        logger.error('Provisioning timeout exceeded')\n        return {'status': 'timeout', 'device': device_mac}","explanation":"The function accepts untrusted input parameters (device_mac, firmware_url, config_params) and directly interpolates them into shell commands without any sanitization or validation. These tainted values flow through string concatenation operations to build the flash_cmd variable, which is then executed via subprocess.run() with shell=True, enabling command injection attacks.","remediation":"The fix eliminates command injection by replacing the single shell=True subprocess call with multiple individual subprocess calls using argument lists (shell=False), strict input validation with regex patterns for MAC addresses, URLs, and config parameters, and writing configuration files using Python's native file I/O instead of shell echo redirection. Each operation is isolated and uses parameterized arguments that cannot be interpreted as shell commands.","secure_code":"import subprocess\nimport logging\nimport re\nimport shlex\nfrom urllib.parse import urlparse\n\n\ndef validate_mac_address(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, mac):\n        raise ValueError(f'Invalid MAC address format: {mac}')\n    return mac\n\n\ndef validate_firmware_url(url):\n    \"\"\"Validate firmware URL against allowed schemes and domains.\"\"\"\n    parsed = urlparse(url)\n    if parsed.scheme not in ('http', 'https'):\n        raise ValueError(f'Invalid URL scheme: {parsed.scheme}')\n    if not parsed.hostname:\n        raise ValueError('URL must have a valid hostname')\n    if any(c in url for c in [';', '|', '&', '`', '$', '(', ')', '{', '}', '\\n', '\\r']):\n        raise ValueError('URL contains invalid characters')\n    return url\n\n\ndef validate_config_params(config_params):\n    \"\"\"Validate configuration parameters - allow only alphanumeric, common config characters.\"\"\"\n    pattern = r'^[a-zA-Z0-9_=.,:/\\-\\s]+$'\n    if not re.match(pattern, config_params):\n        raise ValueError('Config parameters contain invalid characters')\n    return config_params\n\n\ndef provision_iot_device(device_mac, firmware_url, config_params):\n    logger = logging.getLogger('iot_provisioner')\n    logger.info(f'Provisioning device: {device_mac}')\n\n    try:\n        device_mac = validate_mac_address(device_mac)\n        firmware_url = validate_firmware_url(firmware_url)\n        config_params = validate_config_params(config_params)\n    except ValueError as e:\n        logger.error(f'Input validation failed: {e}')\n        return {'status': 'error', 'message': f'Validation failed: {e}'}\n\n    try:\n        curl_result = subprocess.run(\n            ['curl', '-s', '--max-time', '60', '-o', '/tmp/firmware.bin', firmware_url],\n            capture_output=True, timeout=60\n        )\n        if curl_result.returncode != 0:\n            logger.error(f'Firmware download failed: {curl_result.stderr.decode()}')\n            return {'status': 'error', 'message': 'Firmware download failed'}\n\n        dd_result = subprocess.run(\n            ['dd', 'if=/tmp/firmware.bin', 'of=/dev/mtdblock0', 'bs=1M'],\n            capture_output=True, timeout=60\n        )\n        if dd_result.returncode != 0:\n            logger.error(f'Firmware flash failed: {dd_result.stderr.decode()}')\n            return {'status': 'error', 'message': 'Firmware flash failed'}\n\n        try:\n            with open('/etc/iot/device.conf', 'w') as conf_file:\n                conf_file.write(config_params)\n        except IOError as e:\n            logger.error(f'Config write failed: {e}')\n            return {'status': 'error', 'message': 'Config write failed'}\n\n        mac_result = subprocess.run(\n            ['ip', 'link', 'set', 'dev', 'eth0', 'address', device_mac],\n            capture_output=True, timeout=10\n        )\n        if mac_result.returncode != 0:\n            logger.error(f'MAC address set failed: {mac_result.stderr.decode()}')\n            return {'status': 'error', 'message': 'MAC address configuration failed'}\n\n        logger.info(f'Device {device_mac} provisioned successfully')\n        return {'status': 'success', 'device': device_mac}\n\n    except subprocess.TimeoutExpired:\n        logger.error('Provisioning timeout exceeded')\n        return {'status': 'timeout', 'device': device_mac}\n    except Exception as e:\n        logger.error(f'Unexpected error during provisioning: {e}')\n        return {'status': 'error', 'message': 'Unexpected provisioning error'}"}