{"title":"Zip Slip via unsafe tarfile.extractall() extraction","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[9],"flow_lines":[9,11,12,13,14],"sink_lines":[14],"vulnerable_code":"import tarfile\nimport os\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/cloud/restore-backup', methods=['POST'])\ndef restore_cloud_backup():\n    backup_file = request.files['backup']\n    tenant_id = request.form.get('tenant_id')\n    restore_path = f'/var/cloud/tenants/{tenant_id}/data'\n    os.makedirs(restore_path, exist_ok=True)\n    backup_location = f'/tmp/{backup_file.filename}'\n    backup_file.save(backup_location)\n    with tarfile.open(backup_location, 'r:gz') as tar_archive:\n        tar_archive.extractall(path=restore_path)\n    return jsonify({'status': 'restored', 'tenant': tenant_id, 'files': len(os.listdir(restore_path))})","explanation":"The code uses tarfile.extractall() without validating archive member paths, allowing Zip Slip attacks. A malicious tar.gz archive containing paths like '../../../etc/cron.d/malicious' can traverse outside the intended restore_path directory and overwrite arbitrary files on the server.","remediation":"The fix validates every tar archive member's resolved path before extraction, ensuring no file can be written outside the intended restore directory. It also sanitizes the tenant_id input to prevent path traversal through the directory construction, sanitizes the uploaded filename, and cleans up temporary files after processing.","secure_code":"import tarfile\nimport os\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n\ndef is_safe_tar_member(member, extract_path):\n    \"\"\"Validate that a tar member extracts safely within the target directory.\"\"\"\n    member_path = os.path.realpath(os.path.join(extract_path, member.name))\n    extract_path_real = os.path.realpath(extract_path)\n    if not member_path.startswith(extract_path_real + os.sep) and member_path != extract_path_real:\n        return False\n    if member.issym() or member.islnk():\n        link_target = os.path.realpath(os.path.join(extract_path, member.linkname))\n        if not link_target.startswith(extract_path_real + os.sep) and link_target != extract_path_real:\n            return False\n    return True\n\n\ndef safe_extract(tar_archive, extract_path):\n    \"\"\"Extract tar members only if all paths are safe.\"\"\"\n    for member in tar_archive.getmembers():\n        if not is_safe_tar_member(member, extract_path):\n            raise ValueError(f\"Unsafe path detected in archive: {member.name}\")\n    tar_archive.extractall(path=extract_path)\n\n\n@app.route('/api/cloud/restore-backup', methods=['POST'])\ndef restore_cloud_backup():\n    backup_file = request.files['backup']\n    tenant_id = request.form.get('tenant_id')\n    # Sanitize tenant_id to prevent path traversal via tenant_id itself\n    if not tenant_id or '..' in tenant_id or '/' in tenant_id or '\\\\' in tenant_id:\n        return jsonify({'error': 'Invalid tenant_id'}), 400\n    restore_path = f'/var/cloud/tenants/{tenant_id}/data'\n    os.makedirs(restore_path, exist_ok=True)\n    # Sanitize the filename to prevent path traversal in temp storage\n    safe_filename = os.path.basename(backup_file.filename)\n    backup_location = f'/tmp/{safe_filename}'\n    backup_file.save(backup_location)\n    try:\n        with tarfile.open(backup_location, 'r:gz') as tar_archive:\n            safe_extract(tar_archive, restore_path)\n    except (ValueError, tarfile.TarError) as e:\n        os.remove(backup_location)\n        return jsonify({'error': f'Invalid backup archive: {str(e)}'}), 400\n    finally:\n        if os.path.exists(backup_location):\n            os.remove(backup_location)\n    return jsonify({'status': 'restored', 'tenant': tenant_id, 'files': len(os.listdir(restore_path))})"}