# Marshal Deserialization via marshal.loads() on Untrusted Data

Language: Python
Severity: Critical
CWE: CWE-502

## Source
10

## Flow
10-11-12

## Sink
12

## Vulnerable Code
```python
import marshal
import base64
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/iot/device/config', methods=['POST'])
def apply_device_configuration():
    device_id = request.json.get('device_id')
    encoded_cfg = request.json.get('config_payload')
    cfg_bytes = base64.b64decode(encoded_cfg)
    device_config = marshal.loads(cfg_bytes)
    result = execute_config(device_config)
    return jsonify({'device': device_id, 'status': 'applied', 'result': result})

def execute_config(cfg):
    return cfg.get('settings', {})
```

## Explanation

The application accepts untrusted base64-encoded data from a remote IoT device via the 'config_payload' parameter, decodes it, and directly deserializes it using marshal.loads(). Python's marshal module is not designed for untrusted data and can execute arbitrary code during deserialization, allowing remote code execution.

## Remediation

The fix replaces marshal.loads() with json.loads(), which is a safe deserialization format that cannot execute arbitrary code. Additionally, input validation is added including a whitelist of allowed configuration keys, recursive type checking of values with depth limiting, and proper error handling for malformed payloads.

## Secure Code
```python
import json
import base64
from flask import Flask, request, jsonify

app = Flask(__name__)

ALLOWED_CONFIG_KEYS = {'settings', 'firmware_version', 'update_interval', 'network', 'logging'}
ALLOWED_VALUE_TYPES = (str, int, float, bool, list, dict, type(None))

@app.route('/api/iot/device/config', methods=['POST'])
def apply_device_configuration():
    device_id = request.json.get('device_id')
    encoded_cfg = request.json.get('config_payload')
    
    if not device_id or not encoded_cfg:
        return jsonify({'error': 'Missing required fields'}), 400
    
    try:
        cfg_bytes = base64.b64decode(encoded_cfg)
        device_config = json.loads(cfg_bytes)
    except (base64.binascii.Error, json.JSONDecodeError) as e:
        return jsonify({'error': 'Invalid configuration payload format'}), 400
    
    if not isinstance(device_config, dict):
        return jsonify({'error': 'Configuration must be a JSON object'}), 400
    
    unexpected_keys = set(device_config.keys()) - ALLOWED_CONFIG_KEYS
    if unexpected_keys:
        return jsonify({'error': f'Unexpected configuration keys: {unexpected_keys}'}), 400
    
    if not validate_config_values(device_config):
        return jsonify({'error': 'Invalid configuration value types'}), 400
    
    result = execute_config(device_config)
    return jsonify({'device': device_id, 'status': 'applied', 'result': result})


def validate_config_values(obj, depth=0):
    """Recursively validate that config values are safe primitive types with depth limit."""
    if depth > 5:
        return False
    if isinstance(obj, dict):
        return all(
            isinstance(k, str) and validate_config_values(v, depth + 1)
            for k, v in obj.items()
        )
    if isinstance(obj, list):
        return all(validate_config_values(item, depth + 1) for item in obj)
    return isinstance(obj, ALLOWED_VALUE_TYPES)


def execute_config(cfg):
    return cfg.get('settings', {})
```
