# 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, jsonify

app = Flask(__name__)

@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    encoded_state = request.json.get('device_state')
    if encoded_state:
        serialized_config = base64.b64decode(encoded_state)
        device_obj = pickle.loads(serialized_config)
        device_obj.apply_settings()
        return jsonify({'status': 'restored', 'device_id': device_obj.id})
    return jsonify({'error': 'missing state'}), 400
```

## Explanation

The application accepts untrusted user input containing a base64-encoded pickled object, decodes it, and directly deserializes it using pickle.loads(). Pickle deserialization of untrusted data allows remote code execution because pickle can instantiate arbitrary objects and execute code through __reduce__ methods during deserialization.

## Remediation

The fix replaces pickle deserialization with JSON parsing, which cannot execute arbitrary code during deserialization. Additionally, it adds HMAC signature verification to ensure data integrity, a whitelist-based validation of configuration fields, and a safe DeviceConfig class that only accepts known properties rather than deserializing arbitrary objects.

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

app = Flask(__name__)

SIGNING_SECRET = b'your-secure-signing-secret-here'

ALLOWED_CONFIG_FIELDS = {
    'id', 'name', 'firmware_version', 'network_settings',
    'sensor_config', 'reporting_interval', 'power_mode',
    'thresholds', 'enabled_features'
}


class DeviceConfig:
    """Safe device configuration object with validated fields."""

    def __init__(self, config_dict):
        self.id = config_dict.get('id')
        self.name = config_dict.get('name')
        self.firmware_version = config_dict.get('firmware_version')
        self.network_settings = config_dict.get('network_settings', {})
        self.sensor_config = config_dict.get('sensor_config', {})
        self.reporting_interval = config_dict.get('reporting_interval')
        self.power_mode = config_dict.get('power_mode')
        self.thresholds = config_dict.get('thresholds', {})
        self.enabled_features = config_dict.get('enabled_features', [])

    def apply_settings(self):
        """Apply validated device settings."""
        pass


def compute_signature(data: bytes) -> str:
    """Compute HMAC signature for data integrity verification."""
    return hmac.new(SIGNING_SECRET, data, hashlib.sha256).hexdigest()


def verify_signature(data: bytes, signature: str) -> bool:
    """Verify HMAC signature to ensure data integrity."""
    expected = hmac.new(SIGNING_SECRET, data, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


def validate_config(config_dict):
    """Validate that the configuration only contains allowed fields and safe values."""
    if not isinstance(config_dict, dict):
        return False
    if not set(config_dict.keys()).issubset(ALLOWED_CONFIG_FIELDS):
        return False
    if 'id' not in config_dict or not isinstance(config_dict['id'], str):
        return False
    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 jsonify({'error': 'missing state or signature'}), 400

    try:
        raw_data = base64.b64decode(encoded_state)
    except Exception:
        return jsonify({'error': 'invalid base64 encoding'}), 400

    if not verify_signature(raw_data, signature):
        return jsonify({'error': 'invalid signature - data may be tampered'}), 403

    try:
        config_dict = json.loads(raw_data)
    except (json.JSONDecodeError, ValueError):
        return jsonify({'error': 'invalid configuration format'}), 400

    if not validate_config(config_dict):
        return jsonify({'error': 'invalid configuration fields'}), 400

    device_obj = DeviceConfig(config_dict)
    device_obj.apply_settings()

    return jsonify({'status': 'restored', 'device_id': device_obj.id})
```
