# Command Injection via os.system() with Untrusted Input

Language: Python
Severity: Critical
CWE: CWE-78

## Source
4-7

## Flow
4-11, 5-16, 4-19, 6-16, 7-16

## Sink
11, 16, 19

## Vulnerable Code
```python
import os
import json

def provision_iot_device(device_config):
    device_id = device_config.get('device_id')
    firmware_version = device_config.get('firmware_ver')
    network_ssid = device_config.get('wifi_ssid')
    device_type = device_config.get('type')
    
    config_path = f"/opt/iot/devices/{device_id}/config.json"
    os.system(f"mkdir -p /opt/iot/devices/{device_id}")
    
    with open(config_path, 'w') as cfg:
        json.dump(device_config, cfg)
    
    provisioning_cmd = f"iot-provision --device={device_id} --firmware={firmware_version} --ssid={network_ssid} --type={device_type}"
    result = os.system(provisioning_cmd)
    
    if result == 0:
        os.system(f"echo 'Device {device_id} provisioned successfully' >> /var/log/iot_provision.log")
        return {"status": "success", "device_id": device_id}
    else:
        return {"status": "failed", "device_id": device_id}
```

## Explanation

The function accepts untrusted input through device_config parameters (device_id, firmware_version, network_ssid, device_type) and directly interpolates them into shell commands executed via os.system() without any validation or sanitization. An attacker can inject arbitrary shell commands through any of these parameters using shell metacharacters like semicolons, pipes, or backticks.

## Remediation

The fix replaces all os.system() calls with safe alternatives: subprocess.run() with argument lists (preventing shell interpretation), os.makedirs() for directory creation, and Python's logging module for log writing. All input parameters are validated against strict regex patterns before use, rejecting any values containing shell metacharacters or unexpected characters.

## Secure Code
```python
import os
import json
import subprocess
import re
import logging

logger = logging.getLogger('iot_provision')
logging.basicConfig(filename='/var/log/iot_provision.log', level=logging.INFO)

def validate_device_id(device_id):
    if not device_id or not re.match(r'^[a-zA-Z0-9_\-]+$', device_id):
        raise ValueError(f"Invalid device_id: must be alphanumeric with hyphens/underscores only")
    return device_id

def validate_firmware_version(firmware_ver):
    if not firmware_ver or not re.match(r'^[a-zA-Z0-9\.\-]+$', firmware_ver):
        raise ValueError(f"Invalid firmware version: must be alphanumeric with dots/hyphens only")
    return firmware_ver

def validate_ssid(ssid):
    if not ssid or len(ssid) > 32 or any(c in ssid for c in [';', '|', '&', '`', '$', '(', ')', '{', '}', '\n', '\r']):
        raise ValueError(f"Invalid SSID: contains disallowed characters or exceeds length")
    return ssid

def validate_device_type(device_type):
    if not device_type or not re.match(r'^[a-zA-Z0-9_\-]+$', device_type):
        raise ValueError(f"Invalid device type: must be alphanumeric with hyphens/underscores only")
    return device_type

def provision_iot_device(device_config):
    device_id = validate_device_id(device_config.get('device_id'))
    firmware_version = validate_firmware_version(device_config.get('firmware_ver'))
    network_ssid = validate_ssid(device_config.get('wifi_ssid'))
    device_type = validate_device_type(device_config.get('type'))
    
    config_dir = os.path.join('/opt/iot/devices', device_id)
    config_path = os.path.join(config_dir, 'config.json')
    
    os.makedirs(config_dir, exist_ok=True)
    
    with open(config_path, 'w') as cfg:
        json.dump(device_config, cfg)
    
    provisioning_cmd = [
        'iot-provision',
        f'--device={device_id}',
        f'--firmware={firmware_version}',
        f'--ssid={network_ssid}',
        f'--type={device_type}'
    ]
    result = subprocess.run(provisioning_cmd, capture_output=True, text=True)
    
    if result.returncode == 0:
        logger.info(f'Device {device_id} provisioned successfully')
        return {"status": "success", "device_id": device_id}
    else:
        logger.error(f'Device {device_id} provisioning failed: {result.stderr}')
        return {"status": "failed", "device_id": device_id}
```
