{"title":"Path Traversal via Unsanitized zipfile.extractall()","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"import zipfile, os\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\nFW_STAGING = '/var/iot/firmware/staging'\n@app.route('/api/v1/device/firmware/push', methods=['POST'])\ndef push_fw_bundle():\n    pkg = request.files.get('fw_pkg')\n    if not pkg or not pkg.filename.endswith('.zip'):\n        return jsonify({'err': 'invalid package'}), 400\n    tmp_path = os.path.join('/tmp', pkg.filename)\n    pkg.save(tmp_path)\n    with zipfile.ZipFile(tmp_path, 'r') as zf:\n        zf.extractall(FW_STAGING)\n    return jsonify({'status': 'firmware staged', 'dest': FW_STAGING}), 200","explanation":"The uploaded file `pkg` from the HTTP request is saved directly to disk and then extracted using `zf.extractall(FW_STAGING)` without validating individual ZIP member filenames. An attacker can craft a ZIP archive containing entries with path traversal sequences (e.g., `../../etc/cron.d/backdoor`), causing files to be written outside the intended `/var/iot/firmware/staging` directory — a classic 'Zip Slip' vulnerability.","remediation":"The fix introduces a `_safe_extract` helper that resolves the real (canonical) path of every ZIP member and verifies it stays within the intended destination directory before any extraction occurs. If any entry would escape the staging directory (via `../` or absolute paths), a ValueError is raised and extraction is aborted. Additionally, the uploaded filename is sanitized with `os.path.basename()` to prevent path traversal when saving the temporary file, and the temp file is cleaned up in a `finally` block.","secure_code":"import zipfile, os\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\nFW_STAGING = '/var/iot/firmware/staging'\n\ndef _safe_extract(zf, dest):\n    \"\"\"Extract ZIP members only if they resolve within the destination directory.\"\"\"\n    dest = os.path.realpath(dest)\n    for member in zf.infolist():\n        target_path = os.path.realpath(os.path.join(dest, member.filename))\n        if not target_path.startswith(dest + os.sep) and target_path != dest:\n            raise ValueError(\n                f\"Zip entry '{member.filename}' would escape the destination directory\"\n            )\n    # All members validated; now extract\n    for member in zf.infolist():\n        zf.extract(member, dest)\n\n@app.route('/api/v1/device/firmware/push', methods=['POST'])\ndef push_fw_bundle():\n    pkg = request.files.get('fw_pkg')\n    if not pkg or not pkg.filename.endswith('.zip'):\n        return jsonify({'err': 'invalid package'}), 400\n    # Sanitize the uploaded filename to prevent path traversal on save\n    safe_name = os.path.basename(pkg.filename)\n    if not safe_name:\n        return jsonify({'err': 'invalid filename'}), 400\n    tmp_path = os.path.join('/tmp', safe_name)\n    pkg.save(tmp_path)\n    try:\n        with zipfile.ZipFile(tmp_path, 'r') as zf:\n            _safe_extract(zf, FW_STAGING)\n    except (ValueError, zipfile.BadZipFile) as exc:\n        return jsonify({'err': str(exc)}), 400\n    finally:\n        os.remove(tmp_path)\n    return jsonify({'status': 'firmware staged', 'dest': FW_STAGING}), 200"}