{"title":"Tar Slip Path Traversal via tarfile.extractall","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[8],"flow_lines":[8,11,13,14],"sink_lines":[14],"vulnerable_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\n@app.route('/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    device_id = request.form.get('device_id')\n    firmware_archive = request.files['firmware']\n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    archive_path = f'/tmp/{firmware_archive.filename}'\n    firmware_archive.save(archive_path)\n    with tarfile.open(archive_path, 'r:gz') as tar_pkg:\n        tar_pkg.extractall(path=deployment_path)\n    return jsonify({'status': 'deployed', 'device': device_id, 'location': deployment_path})","explanation":"The application extracts a user-uploaded tar.gz archive using tarfile.extractall() without validating the paths of archive members. A malicious archive can contain entries with absolute paths or path traversal sequences (e.g., ../../etc/cron.d/backdoor) that escape the intended deployment_path directory, allowing arbitrary file writes anywhere on the filesystem.","remediation":"The fix validates each tar archive member's resolved path to ensure it stays within the intended deployment directory before extraction. It checks for absolute paths, relative path traversal sequences (e.g., '../'), and symlink targets that could escape the deployment directory. Any malicious member triggers a ValueError, preventing extraction entirely.","secure_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\n@app.route('/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    device_id = request.form.get('device_id')\n    firmware_archive = request.files['firmware']\n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    archive_path = f'/tmp/{firmware_archive.filename}'\n    firmware_archive.save(archive_path)\n    with tarfile.open(archive_path, 'r:gz') as tar_pkg:\n        safe_members = []\n        for member in tar_pkg.getmembers():\n            member_path = os.path.normpath(os.path.join(deployment_path, member.name))\n            if not member_path.startswith(os.path.realpath(deployment_path) + os.sep) and member_path != os.path.realpath(deployment_path):\n                raise ValueError(f\"Attempted path traversal in tar member: {member.name}\")\n            if member.issym() or member.islnk():\n                link_target = os.path.normpath(os.path.join(deployment_path, os.path.dirname(member.name), member.linkname))\n                if not link_target.startswith(os.path.realpath(deployment_path) + os.sep):\n                    raise ValueError(f\"Attempted path traversal via symlink in tar member: {member.name}\")\n            safe_members.append(member)\n        tar_pkg.extractall(path=deployment_path, members=safe_members)\n    os.remove(archive_path)\n    return jsonify({'status': 'deployed', 'device': device_id, 'location': deployment_path})"}