{"title":"Insecure Deserialization via shelve.open() on Untrusted Data","language":"Python","severity":"Critical","cwe":"CWE-502","source_lines":[7,8],"flow_lines":[7,8,9,10],"sink_lines":[10],"vulnerable_code":"import shelve\nimport os\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_config():\n    device_id = request.form.get('device_id')\n    backup_path = request.form.get('backup_file')\n    full_path = f'/tmp/iot_backups/{backup_path}'\n    with shelve.open(full_path) as db:\n        config = db.get(device_id, {})\n    return jsonify({'status': 'restored', 'device': device_id, 'config': config})\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=5000)","explanation":"The application accepts user-controlled input for the backup file path and directly uses it with shelve.open(). Since shelve uses pickle for serialization, an attacker can craft a malicious shelve database containing pickled Python objects that execute arbitrary code upon deserialization when the file is opened.","remediation":"The fix replaces shelve (which uses pickle internally) with JSON for safe deserialization, eliminating the insecure deserialization vulnerability. Additionally, strict input validation is applied to the backup filename using an allowlist regex and path traversal checks with realpath verification to prevent directory traversal attacks.","secure_code":"import json\nimport os\nimport re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\nBACKUP_DIR = '/tmp/iot_backups'\n\n\ndef is_safe_filename(filename):\n    \"\"\"Validate that the filename is safe and doesn't contain path traversal.\"\"\"\n    if not filename:\n        return False\n    # Only allow alphanumeric characters, hyphens, underscores, and dots\n    if not re.match(r'^[a-zA-Z0-9_\\-]+$', filename):\n        return False\n    # Reject path separators and traversal attempts\n    if '..' in filename or '/' in filename or '\\\\' in filename:\n        return False\n    return True\n\n\n@app.route('/iot/device/restore', methods=['POST'])\ndef restore_device_config():\n    device_id = request.form.get('device_id')\n    backup_name = request.form.get('backup_file')\n\n    if not device_id or not backup_name:\n        return jsonify({'status': 'error', 'message': 'Missing device_id or backup_file'}), 400\n\n    # Validate the backup filename to prevent path traversal\n    if not is_safe_filename(backup_name):\n        return jsonify({'status': 'error', 'message': 'Invalid backup file name'}), 400\n\n    # Construct path and verify it resolves within the allowed directory\n    full_path = os.path.join(BACKUP_DIR, f'{backup_name}.json')\n    real_path = os.path.realpath(full_path)\n    if not real_path.startswith(os.path.realpath(BACKUP_DIR) + os.sep):\n        return jsonify({'status': 'error', 'message': 'Invalid backup file path'}), 400\n\n    if not os.path.exists(full_path):\n        return jsonify({'status': 'error', 'message': 'Backup file not found'}), 404\n\n    # Use JSON instead of shelve/pickle for safe deserialization\n    try:\n        with open(full_path, 'r') as f:\n            backup_data = json.load(f)\n    except (json.JSONDecodeError, IOError) as e:\n        return jsonify({'status': 'error', 'message': 'Failed to read backup file'}), 500\n\n    config = backup_data.get(device_id, {})\n    return jsonify({'status': 'restored', 'device': device_id, 'config': config})\n\n\nif __name__ == '__main__':\n    os.makedirs(BACKUP_DIR, exist_ok=True)\n    app.run(host='0.0.0.0', port=5000)"}