# Unsafe `marshal.loads()` Deserialization

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')
    firmware_ver = device_params.get('firmware', '1.0')
    return jsonify({'status': 'configured', 'device': device_id, 'version': firmware_ver})
```

## Explanation

The application accepts user-controlled base64-encoded data via request.json.get('device_config'), decodes it, and directly deserializes it using marshal.loads(). The marshal module can execute arbitrary code during deserialization of malicious bytecode, allowing remote code execution.

## Remediation

The fix replaces the dangerous marshal.loads() deserialization with json.loads(), which only parses data into safe primitive types (strings, numbers, lists, dicts) and cannot execute arbitrary code. Additionally, input validation, payload size limits, field whitelisting, and type checking are added to enforce a strict contract on acceptable configuration data.

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

app = Flask(__name__)

ALLOWED_FIELDS = {'device_id', 'firmware', 'network', 'polling_interval', 'enabled'}
MAX_PAYLOAD_SIZE = 10240  # 10KB max config size

@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({'error': 'Missing or invalid device_config'}), 400

    try:
        cfg_bytes = base64.b64decode(encoded_cfg)
    except Exception:
        return jsonify({'error': 'Invalid base64 encoding'}), 400

    if len(cfg_bytes) > MAX_PAYLOAD_SIZE:
        return jsonify({'error': 'Configuration payload too large'}), 400

    try:
        device_params = json.loads(cfg_bytes)
    except (json.JSONDecodeError, ValueError):
        return jsonify({'error': 'Invalid JSON configuration'}), 400

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

    # Only allow known configuration fields
    filtered_params = {k: v for k, v in device_params.items() if k in ALLOWED_FIELDS}

    device_id = filtered_params.get('device_id', 'unknown')
    firmware_ver = filtered_params.get('firmware', '1.0')

    # Validate field types
    if not isinstance(device_id, str) or not isinstance(firmware_ver, str):
        return jsonify({'error': 'Invalid field types in configuration'}), 400

    return jsonify({'status': 'configured', 'device': device_id, 'version': firmware_ver})
```
