{"title":"Pickle Deserialization via __reduce__","language":"Python","severity":"Critical","cwe":"CWE-502","source_lines":[10],"flow_lines":[10,13,14,15],"sink_lines":[15],"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_state():\n    device_id = request.json.get('device_id')\n    encoded_state = request.json.get('state_snapshot')\n    \n    if not encoded_state:\n        return jsonify({'error': 'Missing state snapshot'}), 400\n    \n    try:\n        state_data = base64.b64decode(encoded_state)\n        device_config = pickle.loads(state_data)\n        \n        return jsonify({\n            'status': 'restored',\n            'device_id': device_id,\n            'config': str(device_config)\n        }), 200\n    except Exception as e:\n        return jsonify({'error': 'Failed to restore state'}), 500","explanation":"The application accepts user-controlled base64-encoded data via the 'state_snapshot' parameter, decodes it, and directly deserializes it using pickle.loads(). Python's pickle module can execute arbitrary code during deserialization when specially crafted payloads exploit the __reduce__ method, allowing Remote Code Execution (RCE).","remediation":"The fix replaces pickle.loads() with json.loads() for deserialization, which is inherently safe as JSON cannot execute arbitrary code during parsing. Additionally, a validation function checks that the deserialized configuration only contains expected keys with appropriate data types, providing defense-in-depth against malformed input.","secure_code":"import json\nimport base64\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n# Define allowed keys and types for device configuration\nALLOWED_CONFIG_KEYS = {\n    'device_name': str,\n    'firmware_version': str,\n    'network_settings': dict,\n    'sensors': list,\n    'thresholds': dict,\n    'enabled': bool,\n    'update_interval': (int, float),\n    'mode': str\n}\n\ndef validate_device_config(config):\n    \"\"\"Validate that the deserialized config only contains expected keys and types.\"\"\"\n    if not isinstance(config, dict):\n        raise ValueError(\"Configuration must be a dictionary\")\n    \n    for key, value in config.items():\n        if key not in ALLOWED_CONFIG_KEYS:\n            raise ValueError(f\"Unexpected configuration key: {key}\")\n        expected_type = ALLOWED_CONFIG_KEYS[key]\n        if not isinstance(value, expected_type):\n            raise ValueError(f\"Invalid type for key '{key}'\")\n    \n    return config\n\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_state():\n    device_id = request.json.get('device_id')\n    encoded_state = request.json.get('state_snapshot')\n    \n    if not encoded_state:\n        return jsonify({'error': 'Missing state snapshot'}), 400\n    \n    try:\n        state_data = base64.b64decode(encoded_state)\n        # Use JSON for safe deserialization instead of pickle\n        device_config = json.loads(state_data)\n        \n        # Validate the structure of the deserialized config\n        validated_config = validate_device_config(device_config)\n        \n        return jsonify({\n            'status': 'restored',\n            'device_id': device_id,\n            'config': validated_config\n        }), 200\n    except (json.JSONDecodeError, ValueError) as e:\n        return jsonify({'error': f'Invalid state snapshot: {str(e)}'}), 400\n    except Exception as e:\n        return jsonify({'error': 'Failed to restore state'}), 500"}