{"title":"Insecure Deserialization via `ast.literal_eval()` on Untrusted Input","language":"Python","severity":"Medium","cwe":"CWE-502","source_lines":[8],"flow_lines":[8,9],"sink_lines":[9],"vulnerable_code":"from flask import Flask, request, jsonify\nimport ast\napp = Flask(__name__)\n@app.route('/iot/device/config', methods=['POST'])\ndef update_device_config():\n    device_id = request.form.get('device_id')\n    config_payload = request.form.get('config_data')\n    try:\n        parsed_config = ast.literal_eval(config_payload)\n        device_settings = {'device': device_id, 'config': parsed_config}\n        return jsonify({'status': 'success', 'updated': device_settings}), 200\n    except Exception as e:\n        return jsonify({'status': 'error', 'message': str(e)}), 400","explanation":"The application accepts untrusted user input from request.form.get('config_data') at line 8 and directly passes it to ast.literal_eval() at line 9 without validation. While ast.literal_eval() is safer than eval(), it can still cause Denial of Service through deeply nested structures or consume excessive memory with large literals, enabling resource exhaustion attacks.","remediation":"The fix replaces ast.literal_eval() with json.loads() which is the standard and safe way to parse structured data from untrusted input. Additionally, it adds input size validation to prevent memory exhaustion, nesting depth checks to prevent stack overflow from deeply nested structures, and type validation to ensure the parsed config is a valid object or array.","secure_code":"from flask import Flask, request, jsonify\nimport json\n\napp = Flask(__name__)\n\nMAX_CONFIG_SIZE = 10000  # Maximum allowed config payload size in characters\nMAX_NESTING_DEPTH = 10  # Maximum allowed nesting depth\n\n\ndef check_nesting_depth(obj, max_depth, current_depth=0):\n    if current_depth > max_depth:\n        return False\n    if isinstance(obj, dict):\n        for key, value in obj.items():\n            if not check_nesting_depth(value, max_depth, current_depth + 1):\n                return False\n    elif isinstance(obj, (list, tuple)):\n        for item in obj:\n            if not check_nesting_depth(item, max_depth, current_depth + 1):\n                return False\n    return True\n\n\n@app.route('/iot/device/config', methods=['POST'])\ndef update_device_config():\n    device_id = request.form.get('device_id')\n    config_payload = request.form.get('config_data')\n\n    if not config_payload:\n        return jsonify({'status': 'error', 'message': 'Missing config_data'}), 400\n\n    if len(config_payload) > MAX_CONFIG_SIZE:\n        return jsonify({'status': 'error', 'message': 'Config payload exceeds maximum allowed size'}), 400\n\n    try:\n        parsed_config = json.loads(config_payload)\n    except (json.JSONDecodeError, ValueError) as e:\n        return jsonify({'status': 'error', 'message': 'Invalid JSON configuration data'}), 400\n\n    if not isinstance(parsed_config, (dict, list)):\n        return jsonify({'status': 'error', 'message': 'Config must be a JSON object or array'}), 400\n\n    if not check_nesting_depth(parsed_config, MAX_NESTING_DEPTH):\n        return jsonify({'status': 'error', 'message': 'Config nesting depth exceeds maximum allowed'}), 400\n\n    device_settings = {'device': device_id, 'config': parsed_config}\n    return jsonify({'status': 'success', 'updated': device_settings}), 200"}