{"title":"Pickle Deserialization via __reduce__","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\n\napp = Flask(__name__)\n\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_config():\n    encoded_state = request.json.get('device_state')\n    device_data = base64.b64decode(encoded_state)\n    restored_config = pickle.loads(device_data)\n    return {'status': 'restored', 'device_id': restored_config.get('id')}, 200","explanation":"The application accepts user-controlled base64-encoded data via the 'device_state' parameter, decodes it, and directly deserializes it using pickle.loads() without validation. This allows attackers to craft malicious pickled objects with custom __reduce__ methods that execute arbitrary Python code during deserialization.","remediation":"The fix replaces pickle.loads() with json.loads(), which cannot execute arbitrary code during deserialization. Additionally, HMAC signature verification ensures only authentic data from trusted devices is accepted, and input validation confirms the deserialized config conforms to an expected schema with allowed keys only.","secure_code":"import json\nimport base64\nimport hmac\nimport hashlib\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n# Secret key for HMAC validation (should be stored securely, e.g., environment variable)\nDEVICE_HMAC_SECRET = b'your-secret-key-here'  # Replace with secure key from env/vault\n\nALLOWED_CONFIG_KEYS = {'id', 'name', 'firmware_version', 'settings', 'network', 'last_seen'}\n\ndef validate_device_config(config):\n    \"\"\"Validate that the deserialized config contains only expected fields and types.\"\"\"\n    if not isinstance(config, dict):\n        raise ValueError(\"Device config must be a dictionary\")\n    if 'id' not in config:\n        raise ValueError(\"Device config must contain 'id'\")\n    unexpected_keys = set(config.keys()) - ALLOWED_CONFIG_KEYS\n    if unexpected_keys:\n        raise ValueError(f\"Unexpected config keys: {unexpected_keys}\")\n    return True\n\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_config():\n    encoded_state = request.json.get('device_state')\n    signature = request.json.get('signature')\n\n    if not encoded_state or not signature:\n        return {'status': 'error', 'message': 'Missing device_state or signature'}, 400\n\n    # Verify HMAC signature to ensure data integrity and authenticity\n    expected_sig = hmac.new(DEVICE_HMAC_SECRET, encoded_state.encode(), hashlib.sha256).hexdigest()\n    if not hmac.compare_digest(expected_sig, signature):\n        return {'status': 'error', 'message': 'Invalid signature'}, 403\n\n    try:\n        device_data = base64.b64decode(encoded_state)\n        # Use JSON instead of pickle for safe deserialization\n        restored_config = json.loads(device_data)\n        validate_device_config(restored_config)\n    except (ValueError, json.JSONDecodeError) as e:\n        return {'status': 'error', 'message': f'Invalid device state: {str(e)}'}, 400\n\n    return {'status': 'restored', 'device_id': restored_config.get('id')}, 200"}