# Pickle Deserialization via __reduce__

Language: Python
Severity: Critical
CWE: CWE-502

## Source
11

## Flow
11-15-16

## Sink
16

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

app = Flask(__name__)

@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    device_id = request.json.get('device_id')
    encoded_snapshot = request.json.get('config_snapshot')
    if not encoded_snapshot:
        return jsonify({'error': 'Missing configuration snapshot'}), 400
    try:
        snapshot_bytes = base64.b64decode(encoded_snapshot)
        device_config = pickle.loads(snapshot_bytes)
        device_config['device_id'] = device_id
        device_config['restored_at'] = __import__('datetime').datetime.now().isoformat()
        return jsonify({'status': 'success', 'config': device_config, 'message': f'Device {device_id} restored'})
    except Exception as e:
        return jsonify({'error': 'Failed to restore configuration', 'details': str(e)}), 500
```

## Explanation

The application accepts a base64-encoded pickle object from user input (config_snapshot) and directly deserializes it using pickle.loads() without any validation. Python's pickle module can execute arbitrary code during deserialization through specially crafted payloads using __reduce__ or other magic methods, allowing remote code execution.

## Remediation

The fix replaces pickle.loads() with json.loads() for deserialization, eliminating the arbitrary code execution vulnerability since JSON parsing cannot execute code. Additionally, a validation layer checks that configuration keys are from an allowed whitelist and values are safe primitive types, and an optional HMAC signature verification mechanism is added to ensure configuration integrity.

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

app = Flask(__name__)

# Secret key for HMAC signature verification of config snapshots
# In production, load from environment variable or secure vault
CONFIG_SIGNING_KEY = app.config.get('CONFIG_SIGNING_SECRET', 'change-this-to-a-secure-secret-key').encode()

ALLOWED_CONFIG_KEYS = {
    'network', 'wifi_ssid', 'wifi_password', 'firmware_version',
    'update_interval', 'sensors', 'thresholds', 'name',
    'location', 'timezone', 'logging_level', 'enabled_features'
}

def verify_config_signature(encoded_data, signature):
    """Verify HMAC signature of the configuration data."""
    expected_sig = hmac.new(CONFIG_SIGNING_KEY, encoded_data.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected_sig, signature)

def validate_config(config):
    """Validate that the config only contains allowed keys and safe value types."""
    if not isinstance(config, dict):
        return False, "Configuration must be a JSON object"
    for key, value in config.items():
        if key not in ALLOWED_CONFIG_KEYS:
            return False, f"Unrecognized configuration key: {key}"
        if not isinstance(value, (str, int, float, bool, list, dict, type(None))):
            return False, f"Invalid value type for key: {key}"
    return True, None

@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    device_id = request.json.get('device_id')
    encoded_snapshot = request.json.get('config_snapshot')
    signature = request.json.get('signature')
    if not encoded_snapshot:
        return jsonify({'error': 'Missing configuration snapshot'}), 400
    try:
        # Decode base64 and parse as JSON instead of pickle
        snapshot_bytes = base64.b64decode(encoded_snapshot)
        device_config = json.loads(snapshot_bytes)

        # Validate the configuration structure and content
        is_valid, error_msg = validate_config(device_config)
        if not is_valid:
            return jsonify({'error': 'Invalid configuration', 'details': error_msg}), 400

        # Optionally verify signature if provided (recommended for production)
        if signature:
            if not verify_config_signature(encoded_snapshot, signature):
                return jsonify({'error': 'Invalid configuration signature'}), 403

        device_config['device_id'] = device_id
        device_config['restored_at'] = datetime.now().isoformat()
        return jsonify({'status': 'success', 'config': device_config, 'message': f'Device {device_id} restored'})
    except json.JSONDecodeError:
        return jsonify({'error': 'Invalid configuration format', 'details': 'Snapshot must be valid JSON'}), 400
    except Exception as e:
        return jsonify({'error': 'Failed to restore configuration', 'details': str(e)}), 500
```
