{"title":"Tar Slip via tarfile.extractall() on Untrusted Archives","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[7],"flow_lines":[7,10,11],"sink_lines":[11],"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    firmware_archive = request.files['firmware_pkg']\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    temp_tar = f'/tmp/{device_id}_firmware.tar.gz'\n    firmware_archive.save(temp_tar)\n    with tarfile.open(temp_tar, 'r:gz') as tar:\n        tar.extractall(path=deployment_path)\n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})","explanation":"The application accepts an untrusted tar.gz archive uploaded by a user and extracts it using tarfile.extractall() without validating the file paths contained within the archive. A malicious tar archive can contain path traversal sequences (e.g., ../../etc/cron.d/malicious) that allow files to be written outside the intended deployment_path directory, enabling arbitrary file write vulnerabilities.","remediation":"The fix adds a safe extraction function that validates every member's resolved path is within the intended deployment directory before extraction, blocking path traversal attacks via relative paths (../) or absolute paths. It also validates symlinks to prevent indirect traversal, sanitizes the device_id input to prevent injection into the base path, cleans up the temporary file after processing, and returns a 400 status on malicious archive detection.","secure_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\ndef _is_within_directory(directory, target):\n    \"\"\"Check if the target path is within the intended directory.\"\"\"\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, path):\n    \"\"\"Safely extract tar members after validating all paths are within the target directory.\"\"\"\n    for member in tar.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 file: {member.name}\")\n        if member.issym() or member.islnk():\n            link_target = os.path.join(path, os.path.dirname(member.name), member.linkname)\n            if not _is_within_directory(path, link_target):\n                raise Exception(f\"Attempted symlink path traversal in tar file: {member.name} -> {member.linkname}\")\n    tar.extractall(path=path)\n\n@app.route('/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    firmware_archive = request.files['firmware_pkg']\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    temp_tar = f'/tmp/{device_id}_firmware.tar.gz'\n    firmware_archive.save(temp_tar)\n    try:\n        with tarfile.open(temp_tar, 'r:gz') as tar:\n            _safe_extract(tar, deployment_path)\n    except Exception as e:\n        os.remove(temp_tar)\n        return jsonify({'status': 'error', 'message': str(e)}), 400\n    finally:\n        if os.path.exists(temp_tar):\n            os.remove(temp_tar)\n    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})"}