# 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')}, 200
```

## Explanation

The application accepts user-controlled base64-encoded data via the 'device_state' parameter, decodes it, and directly deserializes it using pickle.loads() without validation. This allows attackers to craft malicious pickled objects with custom __reduce__ methods that execute arbitrary Python code during deserialization.

## Remediation

The fix replaces pickle.loads() with json.loads(), which cannot execute arbitrary code during deserialization. Additionally, HMAC signature verification ensures only authentic data from trusted devices is accepted, and input validation confirms the deserialized config conforms to an expected schema with allowed keys only.

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

app = Flask(__name__)

# Secret key for HMAC validation (should be stored securely, e.g., environment variable)
DEVICE_HMAC_SECRET = b'your-secret-key-here'  # Replace with secure key from env/vault

ALLOWED_CONFIG_KEYS = {'id', 'name', 'firmware_version', 'settings', 'network', 'last_seen'}

def validate_device_config(config):
    """Validate that the deserialized config contains only expected fields and types."""
    if not isinstance(config, dict):
        raise ValueError("Device config must be a dictionary")
    if 'id' not in config:
        raise ValueError("Device config must contain 'id'")
    unexpected_keys = set(config.keys()) - ALLOWED_CONFIG_KEYS
    if unexpected_keys:
        raise ValueError(f"Unexpected config keys: {unexpected_keys}")
    return True

@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    encoded_state = request.json.get('device_state')
    signature = request.json.get('signature')

    if not encoded_state or not signature:
        return {'status': 'error', 'message': 'Missing device_state or signature'}, 400

    # Verify HMAC signature to ensure data integrity and authenticity
    expected_sig = hmac.new(DEVICE_HMAC_SECRET, encoded_state.encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected_sig, signature):
        return {'status': 'error', 'message': 'Invalid signature'}, 403

    try:
        device_data = base64.b64decode(encoded_state)
        # Use JSON instead of pickle for safe deserialization
        restored_config = json.loads(device_data)
        validate_device_config(restored_config)
    except (ValueError, json.JSONDecodeError) as e:
        return {'status': 'error', 'message': f'Invalid device state: {str(e)}'}, 400

    return {'status': 'restored', 'device_id': restored_config.get('id')}, 200
```
