# Pickle Deserialization via __reduce__

Language: Python
Severity: Critical
CWE: CWE-502

## Source
10

## Flow
10-14-15

## Sink
15

## 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_config = request.json.get('config_snapshot')
    
    if not encoded_config:
        return jsonify({'error': 'Missing configuration data'}), 400
    
    try:
        config_bytes = base64.b64decode(encoded_config)
        device_config = pickle.loads(config_bytes)
        
        apply_device_settings(device_id, device_config)
        
        return jsonify({
            'status': 'success',
            'device_id': device_id,
            'message': 'Device configuration restored successfully'
        }), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

def apply_device_settings(dev_id, config):
    print(f"Applying settings to device {dev_id}: {config}")
```

## Explanation

The application accepts user-controlled base64-encoded data via the 'config_snapshot' parameter, decodes it, and directly deserializes it using pickle.loads() without validation. Pickle deserialization of untrusted data allows arbitrary code execution because attackers can craft malicious pickle payloads using __reduce__ methods to execute system commands.

## Remediation

The fix replaces pickle.loads() with json.loads(), which cannot execute arbitrary code during deserialization. Additionally, a configuration validation layer with an allowlist of permitted keys and expected types ensures only legitimate device configuration data is accepted. An optional HMAC signature verification is added using the correct Python 3 hmac.HMAC() constructor to ensure configurations were created by the trusted system.

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

app = Flask(__name__)

# Secret key for HMAC signing - should be stored securely (e.g., environment variable)
CONFIG_SIGNING_KEY = b'your-secret-signing-key-store-in-env'

# Allowlist of valid configuration keys and their expected types
ALLOWED_CONFIG_KEYS = {
    'network': dict,
    'firmware_version': str,
    'sensors': list,
    'thresholds': dict,
    'reporting_interval': (int, float),
    'device_name': str,
    'enabled': bool,
    'wifi_ssid': str,
    'wifi_channel': int,
    'mqtt_broker': str,
    'mqtt_port': int,
}


def validate_config(config):
    """Validate that the configuration only contains allowed keys and types."""
    if not isinstance(config, dict):
        raise ValueError("Configuration must be a dictionary")
    
    for key, value in config.items():
        if key not in ALLOWED_CONFIG_KEYS:
            raise ValueError(f"Unknown configuration key: {key}")
        expected_type = ALLOWED_CONFIG_KEYS[key]
        if not isinstance(value, expected_type):
            raise ValueError(f"Invalid type for key '{key}': expected {expected_type}, got {type(value)}")
    
    return True


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


@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    device_id = request.json.get('device_id')
    encoded_config = request.json.get('config_snapshot')
    signature = request.json.get('signature')
    
    if not encoded_config:
        return jsonify({'error': 'Missing configuration data'}), 400
    
    if not device_id or not isinstance(device_id, str):
        return jsonify({'error': 'Invalid device_id'}), 400
    
    try:
        config_bytes = base64.b64decode(encoded_config)
        
        # Optional: Verify HMAC signature if provided (for configs created by our system)
        if signature:
            if not verify_config_signature(config_bytes, signature):
                return jsonify({'error': 'Invalid configuration signature'}), 403
        
        # Use JSON deserialization instead of pickle - safe from arbitrary code execution
        device_config = json.loads(config_bytes.decode('utf-8'))
        
        # Validate the configuration structure and types
        validate_config(device_config)
        
        apply_device_settings(device_id, device_config)
        
        return jsonify({
            'status': 'success',
            'device_id': device_id,
            'message': 'Device configuration restored successfully'
        }), 200
    except (json.JSONDecodeError, UnicodeDecodeError):
        return jsonify({'error': 'Invalid configuration format - must be valid JSON'}), 400
    except ValueError as e:
        return jsonify({'error': f'Configuration validation failed: {str(e)}'}), 400
    except Exception as e:
        return jsonify({'error': 'Internal server error'}), 500


@app.route('/iot/device/backup', methods=['POST'])
def backup_device_config():
    """Create a signed backup of device configuration using safe JSON serialization."""
    device_id = request.json.get('device_id')
    config = request.json.get('config')
    
    if not config or not isinstance(config, dict):
        return jsonify({'error': 'Invalid configuration'}), 400
    
    try:
        validate_config(config)
        config_bytes = json.dumps(config, sort_keys=True).encode('utf-8')
        encoded_config = base64.b64encode(config_bytes).decode('utf-8')
        signature = hmac.HMAC(CONFIG_SIGNING_KEY, config_bytes, hashlib.sha256).hexdigest()
        
        return jsonify({
            'device_id': device_id,
            'config_snapshot': encoded_config,
            'signature': signature
        }), 200
    except ValueError as e:
        return jsonify({'error': str(e)}), 400


def apply_device_settings(dev_id, config):
    print(f"Applying settings to device {dev_id}: {config}")
```
