# Pickle Deserialization via __reduce__ Gadget Execution

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')
    serialized_config = base64.b64decode(encoded_state)
    device_obj = pickle.loads(serialized_config)
    device_obj.apply_settings()
    return jsonify({'status': 'restored', 'device_id': device_obj.device_id})
```

## Explanation

The application accepts base64-encoded serialized data from user input (device_state parameter) and directly deserializes it using pickle.loads() without validation. Pickle deserialization of untrusted data allows attackers to execute arbitrary code by crafting malicious payloads with __reduce__ methods that execute system commands during deserialization.

## Remediation

The fix replaces unsafe pickle deserialization with JSON parsing, which cannot execute arbitrary code during deserialization. Additionally, a strict schema validation layer ensures only expected fields with proper types and value ranges are accepted, and an optional HMAC signature verification ensures data integrity and authenticity from trusted edge devices.

## 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-signing-secret-here'

ALLOWED_DEVICE_FIELDS = {
    'device_id', 'firmware_version', 'network_config', 'sensor_thresholds',
    'reporting_interval', 'power_mode', 'display_settings', 'alert_config'
}

ALLOWED_NETWORK_FIELDS = {'ssid', 'ip_address', 'subnet_mask', 'gateway', 'dns'}


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

    def __init__(self, config_dict):
        self.device_id = config_dict.get('device_id', '')
        self.firmware_version = config_dict.get('firmware_version', '')
        self.network_config = config_dict.get('network_config', {})
        self.sensor_thresholds = config_dict.get('sensor_thresholds', {})
        self.reporting_interval = config_dict.get('reporting_interval', 60)
        self.power_mode = config_dict.get('power_mode', 'normal')
        self.display_settings = config_dict.get('display_settings', {})
        self.alert_config = config_dict.get('alert_config', {})

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


def verify_signature(data_bytes, provided_signature):
    """Verify HMAC signature of the device state payload."""
    expected_signature = hmac.new(SIGNING_SECRET, data_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected_signature, provided_signature)


def validate_config(config_dict):
    """Validate that the configuration only contains allowed fields and safe values."""
    if not isinstance(config_dict, dict):
        raise ValueError("Configuration must be a JSON object")

    unknown_fields = set(config_dict.keys()) - ALLOWED_DEVICE_FIELDS
    if unknown_fields:
        raise ValueError(f"Unknown configuration fields: {unknown_fields}")

    if 'device_id' not in config_dict or not isinstance(config_dict['device_id'], str):
        raise ValueError("Valid device_id is required")

    if len(config_dict['device_id']) > 64:
        raise ValueError("device_id exceeds maximum length")

    if 'network_config' in config_dict:
        if not isinstance(config_dict['network_config'], dict):
            raise ValueError("network_config must be a JSON object")
        unknown_net_fields = set(config_dict['network_config'].keys()) - ALLOWED_NETWORK_FIELDS
        if unknown_net_fields:
            raise ValueError(f"Unknown network fields: {unknown_net_fields}")

    if 'reporting_interval' in config_dict:
        interval = config_dict['reporting_interval']
        if not isinstance(interval, (int, float)) or interval < 1 or interval > 86400:
            raise ValueError("reporting_interval must be between 1 and 86400 seconds")

    if 'power_mode' in config_dict:
        if config_dict['power_mode'] not in ('normal', 'low_power', 'sleep', 'performance'):
            raise ValueError("Invalid power_mode value")

    return True


@app.route('/iot/device/restore', methods=['POST'])
def restore_device_config():
    request_data = request.json
    if not request_data or 'device_state' not in request_data:
        return jsonify({'status': 'error', 'message': 'Missing device_state'}), 400

    encoded_state = request_data.get('device_state')
    signature = request_data.get('signature', '')

    try:
        config_bytes = base64.b64decode(encoded_state)
    except Exception:
        return jsonify({'status': 'error', 'message': 'Invalid base64 encoding'}), 400

    if signature:
        if not verify_signature(config_bytes, signature):
            return jsonify({'status': 'error', 'message': 'Invalid signature'}), 403

    try:
        config_dict = json.loads(config_bytes)
    except json.JSONDecodeError:
        return jsonify({'status': 'error', 'message': 'Invalid JSON configuration'}), 400

    try:
        validate_config(config_dict)
    except ValueError as e:
        return jsonify({'status': 'error', 'message': str(e)}), 400

    device_obj = DeviceConfig(config_dict)
    device_obj.apply_settings()

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