# Code Injection via exec() on Untrusted Input

Language: Python
Severity: Critical
CWE: CWE-94

## Source
3

## Flow
3-14

## Sink
14

## Vulnerable Code
```python
@app.route('/api/iot/device/configure', methods=['POST'])
def configure_iot_device():
    device_id = request.json.get('device_id')
    config_script = request.json.get('config_commands')
    device_registry = load_device_registry()
    if device_id not in device_registry:
        return jsonify({'error': 'Device not found'}), 404
    device_context = {
        'device': device_registry[device_id],
        'set_param': lambda k, v: device_registry[device_id].update({k: v}),
        'get_status': lambda: device_registry[device_id].get('status'),
        'reboot': lambda: trigger_device_reboot(device_id)
    }
    try:
        exec(config_script, device_context)
        save_device_registry(device_registry)
        return jsonify({'status': 'configured', 'device_id': device_id}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500
```

## Explanation

The application accepts arbitrary Python code through the 'config_commands' parameter and directly executes it using exec() without any validation or sanitization. An attacker can inject malicious Python code that will be executed with the application's privileges, potentially compromising the entire system, stealing credentials, or manipulating all devices in the registry.

## Remediation

The fix completely removes the dangerous exec() call and replaces it with a safe command dispatcher pattern. Instead of accepting arbitrary Python code, the endpoint now expects a structured list of command objects with predefined 'action' names and validated 'args' dictionaries. Only whitelisted operations (set_param, get_status, reboot, etc.) can be executed, and all input values are validated to be primitive types only.

## Secure Code
```python
@app.route('/api/iot/device/configure', methods=['POST'])
def configure_iot_device():
    device_id = request.json.get('device_id')
    config_commands = request.json.get('config_commands')
    device_registry = load_device_registry()
    if device_id not in device_registry:
        return jsonify({'error': 'Device not found'}), 404

    # Define allowed configuration operations as a safe command dispatcher
    ALLOWED_COMMANDS = {
        'set_param': lambda device, args: device.update({args['key']: args['value']}),
        'get_status': lambda device, args: device.get('status'),
        'reboot': lambda device, args: trigger_device_reboot(device_id),
        'set_name': lambda device, args: device.update({'name': str(args.get('value', ''))}),
        'set_interval': lambda device, args: device.update({'interval': int(args['value'])}) if isinstance(args.get('value'), (int, float)) and 1 <= int(args['value']) <= 86400 else None,
        'enable': lambda device, args: device.update({'status': 'enabled'}),
        'disable': lambda device, args: device.update({'status': 'disabled'}),
    }

    if not isinstance(config_commands, list):
        return jsonify({'error': 'config_commands must be a list of command objects'}), 400

    results = []
    device = device_registry[device_id]

    try:
        for cmd in config_commands:
            if not isinstance(cmd, dict):
                return jsonify({'error': 'Each command must be an object with "action" and optional "args"'}), 400

            action = cmd.get('action')
            args = cmd.get('args', {})

            if not isinstance(action, str) or action not in ALLOWED_COMMANDS:
                return jsonify({'error': f'Unknown or disallowed action: {action}. Allowed: {list(ALLOWED_COMMANDS.keys())}'}), 400

            if not isinstance(args, dict):
                return jsonify({'error': 'Command args must be a dictionary'}), 400

            # Validate that arg values are only primitive types (no code injection via nested objects)
            for key, value in args.items():
                if not isinstance(key, str):
                    return jsonify({'error': 'Argument keys must be strings'}), 400
                if not isinstance(value, (str, int, float, bool, type(None))):
                    return jsonify({'error': f'Argument values must be primitive types, got {type(value).__name__}'}), 400

            result = ALLOWED_COMMANDS[action](device, args)
            results.append({'action': action, 'result': result})

        save_device_registry(device_registry)
        return jsonify({'status': 'configured', 'device_id': device_id, 'results': results}), 200
    except (ValueError, TypeError, KeyError) as e:
        return jsonify({'error': f'Configuration error: {str(e)}'}), 400
    except Exception as e:
        return jsonify({'error': 'Internal configuration error'}), 500
```
