{"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'), 'firmware': restored_config.get('version')}\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=5000)","explanation":"The application accepts user-controlled base64-encoded data via a JSON POST request, decodes it, and directly deserializes it using pickle.loads() without any validation. Pickle deserialization of untrusted data allows arbitrary code execution through specially crafted payloads using __reduce__ or other magic methods.","remediation":"The fix replaces pickle deserialization with JSON parsing, which cannot execute arbitrary code during deserialization. Additionally, it adds input validation to ensure the decoded configuration contains only expected fields and types, and includes an optional HMAC signature verification mechanism for integrity checking of device states.","secure_code":"import json\nimport base64\nimport hmac\nimport hashlib\nfrom flask import Flask, request, abort\n\napp = Flask(__name__)\n\n# In production, load from secure secrets management (e.g., AWS Secrets Manager, HashiCorp Vault)\nDEVICE_STATE_SECRET = b'your-secure-secret-key-here'\n\nALLOWED_FIELDS = {'id', 'version', 'name', 'settings', 'network', 'last_updated'}\n\ndef verify_signature(data: bytes, signature: str) -> bool:\n    \"\"\"Verify HMAC signature of the device state data.\"\"\"\n    expected = hmac.new(DEVICE_STATE_SECRET, data, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, signature)\n\ndef validate_device_config(config: dict) -> bool:\n    \"\"\"Validate that the device configuration contains only expected fields and safe types.\"\"\"\n    if not isinstance(config, dict):\n        return False\n    if not config.get('id') or not config.get('version'):\n        return False\n    if not set(config.keys()).issubset(ALLOWED_FIELDS):\n        return False\n    if not isinstance(config.get('id'), str):\n        return False\n    if not isinstance(config.get('version'), str):\n        return False\n    return True\n\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_config():\n    if not request.json:\n        abort(400, description='Request must contain JSON body')\n\n    encoded_state = request.json.get('device_state')\n    signature = request.json.get('signature')\n\n    if not encoded_state:\n        abort(400, description='Missing device_state')\n\n    try:\n        device_data = base64.b64decode(encoded_state)\n    except Exception:\n        abort(400, description='Invalid base64 encoding')\n\n    if signature:\n        if not verify_signature(device_data, signature):\n            abort(403, description='Invalid signature for device state')\n\n    try:\n        restored_config = json.loads(device_data)\n    except (json.JSONDecodeError, UnicodeDecodeError):\n        abort(400, description='Invalid device state format: must be valid JSON')\n\n    if not validate_device_config(restored_config):\n        abort(400, description='Invalid device configuration structure')\n\n    return {'status': 'restored', 'device_id': restored_config.get('id'), 'firmware': restored_config.get('version')}\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=5000)"}