{"title":"Path Traversal via zipfile.extractall() Zip Slip","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[8],"flow_lines":[8,9,10,12,13,15,17],"sink_lines":[17],"vulnerable_code":"import zipfile\nimport os\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/v1/deploy-model', methods=['POST'])\ndef deploy_ml_model():\n    if 'model_package' not in request.files:\n        return jsonify({'error': 'No model package provided'}), 400\n    model_zip = request.files['model_package']\n    model_id = request.form.get('model_id', 'default_model')\n    deployment_path = f'/opt/ml_models/{model_id}'\n    os.makedirs(deployment_path, exist_ok=True)\n    zip_path = os.path.join('/tmp', f'{model_id}.zip')\n    model_zip.save(zip_path)\n    with zipfile.ZipFile(zip_path, 'r') as archive:\n        archive.extractall(deployment_path)\n    os.remove(zip_path)\n    return jsonify({'status': 'deployed', 'model_id': model_id, 'path': deployment_path}), 200","explanation":"The application extracts a user-uploaded ZIP file without validating the paths of entries within the archive. An attacker can craft a malicious ZIP file containing entries with path traversal sequences (e.g., '../../../../etc/cron.d/malicious') that escape the intended deployment_path directory when extractall() is called, allowing arbitrary file writes on the server filesystem.","remediation":"The fix validates each entry in the ZIP archive before extraction by resolving the full real path and ensuring it stays within the intended deployment directory. Additionally, the model_id input is sanitized with a whitelist regex to prevent path traversal in the directory creation step. If any malicious path is detected, the request is rejected with a 400 error.","secure_code":"import zipfile\nimport os\nimport re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/v1/deploy-model', methods=['POST'])\ndef deploy_ml_model():\n    if 'model_package' not in request.files:\n        return jsonify({'error': 'No model package provided'}), 400\n    model_zip = request.files['model_package']\n    model_id = request.form.get('model_id', 'default_model')\n    # Sanitize model_id to prevent path traversal in directory creation\n    if not re.match(r'^[a-zA-Z0-9_\\-]+$', model_id):\n        return jsonify({'error': 'Invalid model_id. Only alphanumeric characters, hyphens, and underscores are allowed.'}), 400\n    deployment_path = f'/opt/ml_models/{model_id}'\n    os.makedirs(deployment_path, exist_ok=True)\n    zip_path = os.path.join('/tmp', f'{model_id}.zip')\n    model_zip.save(zip_path)\n    with zipfile.ZipFile(zip_path, 'r') as archive:\n        # Validate all entries before extraction to prevent Zip Slip\n        for member in archive.namelist():\n            member_path = os.path.realpath(os.path.join(deployment_path, member))\n            if not member_path.startswith(os.path.realpath(deployment_path) + os.sep) and member_path != os.path.realpath(deployment_path):\n                os.remove(zip_path)\n                return jsonify({'error': f'Malicious path detected in archive: {member}'}), 400\n        archive.extractall(deployment_path)\n    os.remove(zip_path)\n    return jsonify({'status': 'deployed', 'model_id': model_id, 'path': deployment_path}), 200"}