# Pickle Deserialization via __reduce__

Language: Python
Severity: Critical
CWE: CWE-502

## Source
9

## Flow
9-10-11

## Sink
11

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

app = Flask(__name__)

@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    encoded_state = request.json.get('device_state')
    device_data = base64.b64decode(encoded_state)
    restored_config = pickle.loads(device_data)
    return {'status': 'restored', 'device_id': restored_config.get('id'), 'firmware': restored_config.get('version')}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

## Explanation

The application accepts user-controlled base64-encoded data via a JSON POST request, decodes it, and directly deserializes it using pickle.loads() without any validation. Pickle deserialization of untrusted data allows arbitrary code execution through specially crafted payloads using __reduce__ or other magic methods.

## Remediation

The fix replaces pickle deserialization with JSON parsing, which cannot execute arbitrary code during deserialization. Additionally, it adds input validation to ensure the decoded configuration contains only expected fields and types, and includes an optional HMAC signature verification mechanism for integrity checking of device states.

## Secure Code
```python
import json
import base64
import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)

# In production, load from secure secrets management (e.g., AWS Secrets Manager, HashiCorp Vault)
DEVICE_STATE_SECRET = b'your-secure-secret-key-here'

ALLOWED_FIELDS = {'id', 'version', 'name', 'settings', 'network', 'last_updated'}

def verify_signature(data: bytes, signature: str) -> bool:
    """Verify HMAC signature of the device state data."""
    expected = hmac.new(DEVICE_STATE_SECRET, data, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

def validate_device_config(config: dict) -> bool:
    """Validate that the device configuration contains only expected fields and safe types."""
    if not isinstance(config, dict):
        return False
    if not config.get('id') or not config.get('version'):
        return False
    if not set(config.keys()).issubset(ALLOWED_FIELDS):
        return False
    if not isinstance(config.get('id'), str):
        return False
    if not isinstance(config.get('version'), str):
        return False
    return True

@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    if not request.json:
        abort(400, description='Request must contain JSON body')

    encoded_state = request.json.get('device_state')
    signature = request.json.get('signature')

    if not encoded_state:
        abort(400, description='Missing device_state')

    try:
        device_data = base64.b64decode(encoded_state)
    except Exception:
        abort(400, description='Invalid base64 encoding')

    if signature:
        if not verify_signature(device_data, signature):
            abort(403, description='Invalid signature for device state')

    try:
        restored_config = json.loads(device_data)
    except (json.JSONDecodeError, UnicodeDecodeError):
        abort(400, description='Invalid device state format: must be valid JSON')

    if not validate_device_config(restored_config):
        abort(400, description='Invalid device configuration structure')

    return {'status': 'restored', 'device_id': restored_config.get('id'), 'firmware': restored_config.get('version')}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```
