# Pickle Deserialization via pickle.loads() and __reduce__ Gadget Chain

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_config = request.json.get('device_state')
    config_bytes = base64.b64decode(encoded_config)
    device_obj = pickle.loads(config_bytes)
    device_obj.apply_settings()
    return jsonify({'status': 'restored', 'device_id': device_obj.device_id})
```

## Explanation

The application accepts untrusted user input from a POST request (line 9), base64-decodes it (line 10), and deserializes it using pickle.loads() (line 11). Pickle deserialization is inherently unsafe with untrusted data because attackers can craft malicious pickled objects with __reduce__ methods that execute arbitrary code during deserialization.

## Remediation

The fix replaces unsafe pickle deserialization with JSON parsing, which cannot execute arbitrary code during deserialization. Configuration data is validated against a strict schema of allowed fields and types, and a safe DeviceConfig class is constructed from the validated data instead of deserializing arbitrary objects. An optional HMAC signature verification is added to ensure data integrity from trusted sources.

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

app = Flask(__name__)

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

# Define allowed device configuration fields and their expected types
ALLOWED_CONFIG_FIELDS = {
    'device_id': str,
    'firmware_version': str,
    'network_settings': dict,
    'sensor_config': dict,
    'power_mode': str,
    'update_interval': int,
    'thresholds': dict
}


class DeviceConfig:
    """Safe device configuration object constructed from validated JSON data."""

    def __init__(self, config_data):
        self.device_id = config_data.get('device_id', '')
        self.firmware_version = config_data.get('firmware_version', '')
        self.network_settings = config_data.get('network_settings', {})
        self.sensor_config = config_data.get('sensor_config', {})
        self.power_mode = config_data.get('power_mode', 'normal')
        self.update_interval = config_data.get('update_interval', 60)
        self.thresholds = config_data.get('thresholds', {})

    def apply_settings(self):
        # Apply validated device settings safely
        pass


def validate_config_data(config_data):
    """Validate that configuration data only contains allowed fields with correct types."""
    if not isinstance(config_data, dict):
        raise ValueError("Configuration data must be a dictionary")

    for key, value in config_data.items():
        if key not in ALLOWED_CONFIG_FIELDS:
            raise ValueError(f"Unknown configuration field: {key}")
        expected_type = ALLOWED_CONFIG_FIELDS[key]
        if not isinstance(value, expected_type):
            raise ValueError(f"Invalid type for field '{key}': expected {expected_type.__name__}")

    if 'device_id' not in config_data:
        raise ValueError("Missing required field: device_id")

    return True


def verify_signature(data_bytes, provided_signature):
    """Verify HMAC signature to ensure data integrity."""
    expected_signature = hmac.new(SIGNING_SECRET, data_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected_signature, provided_signature)


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

        if not encoded_config:
            return jsonify({'status': 'error', 'message': 'Missing device_state'}), 400

        config_bytes = base64.b64decode(encoded_config)

        # Verify HMAC signature to ensure data was generated by trusted source
        if signature:
            if not verify_signature(config_bytes, signature):
                return jsonify({'status': 'error', 'message': 'Invalid signature'}), 403

        # Deserialize using safe JSON instead of pickle
        config_data = json.loads(config_bytes.decode('utf-8'))

        # Validate the configuration data structure and types
        validate_config_data(config_data)

        # Construct a safe device config object from validated data
        device_obj = DeviceConfig(config_data)
        device_obj.apply_settings()

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

    except (json.JSONDecodeError, UnicodeDecodeError):
        return jsonify({'status': 'error', 'message': 'Invalid configuration format'}), 400
    except ValueError as e:
        return jsonify({'status': 'error', 'message': str(e)}), 400
    except Exception:
        return jsonify({'status': 'error', 'message': 'Internal server error'}), 500
```
