{"title":"Zip Slip via zipfile.extractall() on Untrusted Archives","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[7],"flow_lines":[7,10,11],"sink_lines":[11],"vulnerable_code":"import zipfile\nimport os\nfrom flask import request, jsonify\n\n@app.route('/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    firmware_zip = request.files['firmware_bundle']\n    device_id = request.form.get('device_id')\n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    zip_path = f'/tmp/{firmware_zip.filename}'\n    firmware_zip.save(zip_path)\n    with zipfile.ZipFile(zip_path, 'r') as zf:\n        zf.extractall(deployment_path)\n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})","explanation":"The application accepts an uploaded ZIP file from an untrusted user and extracts it directly using zipfile.extractall() without validating the paths contained within the archive. A malicious actor can craft a ZIP file with entries containing directory traversal sequences (e.g., '../../../etc/cron.d/malicious') to write files outside the intended deployment_path, leading to arbitrary file write and potential remote code execution.","remediation":"The fix validates each entry in the ZIP archive before extraction by resolving its absolute path and ensuring it remains within the intended deployment directory. Additionally, the device_id is validated against a whitelist pattern to prevent path traversal in the deployment path itself. If any archive entry would escape the target directory, the request is rejected with an error.","secure_code":"import zipfile\nimport os\nimport re\nfrom flask import request, jsonify\n\n@app.route('/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    firmware_zip = request.files['firmware_bundle']\n    device_id = request.form.get('device_id')\n    \n    # Validate device_id to prevent path traversal in the deployment path itself\n    if not device_id or not re.match(r'^[a-zA-Z0-9_\\-]+$', device_id):\n        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400\n    \n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    \n    # Resolve the deployment path to its absolute canonical form\n    abs_deployment_path = os.path.realpath(deployment_path)\n    \n    zip_path = f'/tmp/{firmware_zip.filename}'\n    firmware_zip.save(zip_path)\n    \n    with zipfile.ZipFile(zip_path, 'r') as zf:\n        for member in zf.namelist():\n            # Compute the absolute path of the extracted file\n            member_path = os.path.realpath(os.path.join(abs_deployment_path, member))\n            \n            # Ensure the resolved path is within the intended deployment directory\n            if not member_path.startswith(abs_deployment_path + os.sep) and member_path != abs_deployment_path:\n                os.remove(zip_path)\n                return jsonify({\n                    'status': 'error',\n                    'message': f'Malicious path detected in archive: {member}'\n                }), 400\n        \n        # All paths validated, safe to extract\n        zf.extractall(abs_deployment_path)\n    \n    # Clean up the temporary zip file\n    os.remove(zip_path)\n    \n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})"}