# Unsafe Object Deserialization via marshal.loads()

Language: Python
Severity: Critical
CWE: CWE-502

## Source
9

## Flow
9-10-11

## Sink
11

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

app = Flask(__name__)

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

## Explanation

The application accepts untrusted base64-encoded data from device_config parameter, decodes it, and passes it directly to marshal.loads() without validation. The marshal module can execute arbitrary code during deserialization, allowing attackers to achieve Remote Code Execution by crafting malicious serialized payloads.

## Remediation

The fix replaces the unsafe marshal.loads() deserialization with json.loads(), which only parses data into safe primitive types (dicts, lists, strings, numbers, booleans, null) and cannot execute arbitrary code. Additionally, input validation is added to verify the structure of the parsed data, and settings keys are filtered against an allowlist to enforce expected configuration parameters.

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

app = Flask(__name__)

ALLOWED_SETTINGS_KEYS = {'brightness', 'volume', 'wifi_ssid', 'wifi_password', 'firmware_version', 'update_interval', 'timezone', 'language'}

@app.route('/iot/device/config', methods=['POST'])
def apply_device_configuration():
    encoded_cfg = request.json.get('device_config')
    if not encoded_cfg or not isinstance(encoded_cfg, str):
        return jsonify({'status': 'error', 'message': 'Invalid or missing device_config'}), 400

    try:
        cfg_bytes = base64.b64decode(encoded_cfg)
        device_params = json.loads(cfg_bytes)
    except (base64.binascii.Error, json.JSONDecodeError, UnicodeDecodeError) as e:
        return jsonify({'status': 'error', 'message': 'Invalid configuration format'}), 400

    if not isinstance(device_params, dict):
        return jsonify({'status': 'error', 'message': 'Configuration must be a JSON object'}), 400

    device_id = device_params.get('device_id', 'unknown')
    if not isinstance(device_id, str) or len(device_id) > 128:
        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400

    settings = device_params.get('settings', {})
    if not isinstance(settings, dict):
        return jsonify({'status': 'error', 'message': 'Settings must be a JSON object'}), 400

    validated_settings = {k: v for k, v in settings.items() if k in ALLOWED_SETTINGS_KEYS and isinstance(v, (str, int, float, bool))}

    return jsonify({'status': 'configured', 'device': device_id, 'applied': validated_settings})
```
