{"title":"Tar Slip via tarfile.extractall() on Untrusted Archives","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[8],"flow_lines":[8,10,11,12],"sink_lines":[12],"vulnerable_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\n@app.route('/api/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    device_id = request.form.get('device_id')\n    firmware_tar = request.files['firmware_package']\n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    tar_location = f'/tmp/{firmware_tar.filename}'\n    firmware_tar.save(tar_location)\n    with tarfile.open(tar_location, 'r:gz') as tar_archive:\n        tar_archive.extractall(path=deployment_path)\n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})","explanation":"The vulnerability occurs when an untrusted tar archive uploaded via the Flask endpoint is extracted without validating member paths. A malicious archive can contain entries with path traversal sequences (e.g., '../../../etc/cron.d/malicious') that escape the intended deployment directory during extractall(), allowing arbitrary file writes on the system.","remediation":"The fix validates every tar archive member's resolved path to ensure it remains within the intended deployment directory before extraction. It checks both regular file paths and symbolic/hard link targets for path traversal attempts. Additionally, the device_id is validated to be alphanumeric to prevent path injection, and the temporary filename is sanitized using os.path.basename().","secure_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\ndef _is_within_directory(directory, target):\n    abs_directory = os.path.realpath(directory)\n    abs_target = os.path.realpath(target)\n    return abs_target.startswith(abs_directory + os.sep) or abs_target == abs_directory\n\ndef _safe_extract(tar_archive, path):\n    for member in tar_archive.getmembers():\n        member_path = os.path.join(path, member.name)\n        if not _is_within_directory(path, member_path):\n            raise Exception(f\"Attempted path traversal in tar member: {member.name}\")\n        if member.issym() or member.islnk():\n            link_target = os.path.join(path, os.path.normpath(member.linkname)) if not os.path.isabs(member.linkname) else member.linkname\n            if not _is_within_directory(path, link_target):\n                raise Exception(f\"Attempted path traversal via symlink in tar member: {member.name}\")\n    tar_archive.extractall(path=path)\n\n@app.route('/api/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    device_id = request.form.get('device_id')\n    if not device_id or not device_id.isalnum():\n        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400\n    firmware_tar = request.files['firmware_package']\n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    safe_filename = os.path.basename(firmware_tar.filename)\n    tar_location = f'/tmp/{safe_filename}'\n    firmware_tar.save(tar_location)\n    try:\n        with tarfile.open(tar_location, 'r:gz') as tar_archive:\n            _safe_extract(tar_archive, deployment_path)\n    except Exception as e:\n        os.remove(tar_location)\n        return jsonify({'status': 'error', 'message': str(e)}), 400\n    finally:\n        if os.path.exists(tar_location):\n            os.remove(tar_location)\n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})"}