{"title":"Tar Path Traversal via tarfile.extractall() Member Extraction","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[10],"flow_lines":[10,17],"sink_lines":[17],"vulnerable_code":"import tarfile\nimport os\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    firmware_archive = 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    archive_location = f'/tmp/{firmware_archive.filename}'\n    firmware_archive.save(archive_location)\n    with tarfile.open(archive_location, 'r:gz') as tar_bundle:\n        tar_bundle.extractall(path=deployment_path)\n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})","explanation":"The code accepts a tar archive via user upload and extracts it using tarfile.extractall() without validating member paths. A malicious archive can contain path traversal sequences (e.g., ../../etc/cron.d/malicious) allowing attackers to write files outside the intended deployment directory and potentially achieve code execution or system compromise.","remediation":"The fix validates every tar archive member's resolved path to ensure it stays within the intended deployment directory before extraction, rejecting archives containing path traversal sequences or malicious symlinks. Additionally, the device_id input is sanitized to prevent directory traversal in the deployment path itself, and the archived filename is sanitized using os.path.basename().","secure_code":"import tarfile\nimport os\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\ndef is_safe_tar_member(member, destination):\n    \"\"\"Validate that a tar member extracts safely within the destination directory.\"\"\"\n    member_path = os.path.realpath(os.path.join(destination, member.name))\n    destination_path = os.path.realpath(destination)\n    if not member_path.startswith(destination_path + os.sep) and member_path != destination_path:\n        return False\n    if member.issym() or member.islnk():\n        link_target = os.path.realpath(os.path.join(destination, member.linkname))\n        if not link_target.startswith(destination_path + os.sep):\n            return False\n    return True\n\n@app.route('/api/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    firmware_archive = request.files['firmware_bundle']\n    device_id = request.form.get('device_id')\n    if not device_id or '..' in device_id or '/' in device_id or '\\\\' in device_id:\n        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400\n    deployment_path = f'/opt/iot_devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    archive_location = f'/tmp/{os.path.basename(firmware_archive.filename)}'\n    firmware_archive.save(archive_location)\n    try:\n        with tarfile.open(archive_location, 'r:gz') as tar_bundle:\n            for member in tar_bundle.getmembers():\n                if not is_safe_tar_member(member, deployment_path):\n                    os.remove(archive_location)\n                    return jsonify({'status': 'error', 'message': f'Unsafe path detected in archive: {member.name}'}), 400\n            tar_bundle.extractall(path=deployment_path, members=tar_bundle.getmembers())\n    finally:\n        if os.path.exists(archive_location):\n            os.remove(archive_location)\n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})"}