{"title":"Pickle Deserialization via pickle.loads() and __reduce__ Gadget Chain","language":"Python","severity":"Critical","cwe":"CWE-502","source_lines":[9],"flow_lines":[9,10,11],"sink_lines":[11],"vulnerable_code":"import pickle\nimport base64\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_config():\n    encoded_config = request.json.get('device_state')\n    config_bytes = base64.b64decode(encoded_config)\n    device_obj = pickle.loads(config_bytes)\n    device_obj.apply_settings()\n    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":"import json\nimport base64\nimport hmac\nimport hashlib\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n# Secret key for HMAC verification - should be stored securely (e.g., environment variable)\nSIGNING_SECRET = b'your-secure-secret-key-here'  # Replace with secure key from env\n\n# Define allowed device configuration fields and their expected types\nALLOWED_CONFIG_FIELDS = {\n    'device_id': str,\n    'firmware_version': str,\n    'network_settings': dict,\n    'sensor_config': dict,\n    'power_mode': str,\n    'update_interval': int,\n    'thresholds': dict\n}\n\n\nclass DeviceConfig:\n    \"\"\"Safe device configuration object constructed from validated JSON data.\"\"\"\n\n    def __init__(self, config_data):\n        self.device_id = config_data.get('device_id', '')\n        self.firmware_version = config_data.get('firmware_version', '')\n        self.network_settings = config_data.get('network_settings', {})\n        self.sensor_config = config_data.get('sensor_config', {})\n        self.power_mode = config_data.get('power_mode', 'normal')\n        self.update_interval = config_data.get('update_interval', 60)\n        self.thresholds = config_data.get('thresholds', {})\n\n    def apply_settings(self):\n        # Apply validated device settings safely\n        pass\n\n\ndef validate_config_data(config_data):\n    \"\"\"Validate that configuration data only contains allowed fields with correct types.\"\"\"\n    if not isinstance(config_data, dict):\n        raise ValueError(\"Configuration data must be a dictionary\")\n\n    for key, value in config_data.items():\n        if key not in ALLOWED_CONFIG_FIELDS:\n            raise ValueError(f\"Unknown configuration field: {key}\")\n        expected_type = ALLOWED_CONFIG_FIELDS[key]\n        if not isinstance(value, expected_type):\n            raise ValueError(f\"Invalid type for field '{key}': expected {expected_type.__name__}\")\n\n    if 'device_id' not in config_data:\n        raise ValueError(\"Missing required field: device_id\")\n\n    return True\n\n\ndef verify_signature(data_bytes, provided_signature):\n    \"\"\"Verify HMAC signature to ensure data integrity.\"\"\"\n    expected_signature = hmac.new(SIGNING_SECRET, data_bytes, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected_signature, provided_signature)\n\n\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_config():\n    try:\n        encoded_config = request.json.get('device_state')\n        signature = request.json.get('signature', '')\n\n        if not encoded_config:\n            return jsonify({'status': 'error', 'message': 'Missing device_state'}), 400\n\n        config_bytes = base64.b64decode(encoded_config)\n\n        # Verify HMAC signature to ensure data was generated by trusted source\n        if signature:\n            if not verify_signature(config_bytes, signature):\n                return jsonify({'status': 'error', 'message': 'Invalid signature'}), 403\n\n        # Deserialize using safe JSON instead of pickle\n        config_data = json.loads(config_bytes.decode('utf-8'))\n\n        # Validate the configuration data structure and types\n        validate_config_data(config_data)\n\n        # Construct a safe device config object from validated data\n        device_obj = DeviceConfig(config_data)\n        device_obj.apply_settings()\n\n        return jsonify({'status': 'restored', 'device_id': device_obj.device_id})\n\n    except (json.JSONDecodeError, UnicodeDecodeError):\n        return jsonify({'status': 'error', 'message': 'Invalid configuration format'}), 400\n    except ValueError as e:\n        return jsonify({'status': 'error', 'message': str(e)}), 400\n    except Exception:\n        return jsonify({'status': 'error', 'message': 'Internal server error'}), 500"}