{"title":"Marshal Deserialization via marshal.loads() on Untrusted Data","language":"Python","severity":"Critical","cwe":"CWE-502","source_lines":[10],"flow_lines":[10,11,12],"sink_lines":[12],"vulnerable_code":"import marshal\nimport base64\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/iot/device/config', methods=['POST'])\ndef apply_device_configuration():\n    device_id = request.json.get('device_id')\n    encoded_cfg = request.json.get('config_payload')\n    cfg_bytes = base64.b64decode(encoded_cfg)\n    device_config = marshal.loads(cfg_bytes)\n    result = execute_config(device_config)\n    return jsonify({'device': device_id, 'status': 'applied', 'result': result})\n\ndef execute_config(cfg):\n    return cfg.get('settings', {})","explanation":"The application accepts untrusted base64-encoded data from a remote IoT device via the 'config_payload' parameter, decodes it, and directly deserializes it using marshal.loads(). Python's marshal module is not designed for untrusted data and can execute arbitrary code during deserialization, allowing remote code execution.","remediation":"The fix replaces marshal.loads() with json.loads(), which is a safe deserialization format that cannot execute arbitrary code. Additionally, input validation is added including a whitelist of allowed configuration keys, recursive type checking of values with depth limiting, and proper error handling for malformed payloads.","secure_code":"import json\nimport base64\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\nALLOWED_CONFIG_KEYS = {'settings', 'firmware_version', 'update_interval', 'network', 'logging'}\nALLOWED_VALUE_TYPES = (str, int, float, bool, list, dict, type(None))\n\n@app.route('/api/iot/device/config', methods=['POST'])\ndef apply_device_configuration():\n    device_id = request.json.get('device_id')\n    encoded_cfg = request.json.get('config_payload')\n    \n    if not device_id or not encoded_cfg:\n        return jsonify({'error': 'Missing required fields'}), 400\n    \n    try:\n        cfg_bytes = base64.b64decode(encoded_cfg)\n        device_config = json.loads(cfg_bytes)\n    except (base64.binascii.Error, json.JSONDecodeError) as e:\n        return jsonify({'error': 'Invalid configuration payload format'}), 400\n    \n    if not isinstance(device_config, dict):\n        return jsonify({'error': 'Configuration must be a JSON object'}), 400\n    \n    unexpected_keys = set(device_config.keys()) - ALLOWED_CONFIG_KEYS\n    if unexpected_keys:\n        return jsonify({'error': f'Unexpected configuration keys: {unexpected_keys}'}), 400\n    \n    if not validate_config_values(device_config):\n        return jsonify({'error': 'Invalid configuration value types'}), 400\n    \n    result = execute_config(device_config)\n    return jsonify({'device': device_id, 'status': 'applied', 'result': result})\n\n\ndef validate_config_values(obj, depth=0):\n    \"\"\"Recursively validate that config values are safe primitive types with depth limit.\"\"\"\n    if depth > 5:\n        return False\n    if isinstance(obj, dict):\n        return all(\n            isinstance(k, str) and validate_config_values(v, depth + 1)\n            for k, v in obj.items()\n        )\n    if isinstance(obj, list):\n        return all(validate_config_values(item, depth + 1) for item in obj)\n    return isinstance(obj, ALLOWED_VALUE_TYPES)\n\n\ndef execute_config(cfg):\n    return cfg.get('settings', {})"}