{"title":"Tar Slip via tarfile.extractall()","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[7],"flow_lines":[7,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_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 accepts a user-controlled filename from an uploaded file without validation and uses it to save the archive. Subsequently, tarfile.extractall() is called without validating archive member paths, allowing an attacker to craft a malicious tar archive with paths like '../../../etc/cron.d/malicious' to write files outside the intended deployment directory (path traversal/tar slip vulnerability).","remediation":"The fix adds comprehensive path validation for all tar archive members before extraction, ensuring no member can escape the intended deployment directory via relative paths (../) or symlinks. Additionally, the device_id is validated against a whitelist pattern to prevent directory traversal in the deployment path, and the uploaded file is saved with a randomly generated filename to prevent filename-based attacks.","secure_code":"import tarfile\nimport os\nimport re\nimport uuid\nfrom flask import request, jsonify\n\ndef _is_safe_path(base_path, target_path):\n    \"\"\"Verify that the target path is within the base directory.\"\"\"\n    abs_base = os.path.realpath(base_path)\n    abs_target = os.path.realpath(target_path)\n    return abs_target.startswith(abs_base + os.sep) or abs_target == abs_base\n\ndef _validate_tar_members(tar, destination):\n    \"\"\"Validate all tar members to prevent path traversal (tar slip).\"\"\"\n    for member in tar.getmembers():\n        member_path = os.path.join(destination, member.name)\n        if not _is_safe_path(destination, member_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.join(destination, os.path.dirname(member.name), member.linkname)\n            if not _is_safe_path(destination, link_target):\n                raise ValueError(f\"Attempted path traversal via symlink in tar member: {member.name}\")\n    return tar.getmembers()\n\n@app.route('/api/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    device_id = request.form.get('device_id')\n\n    # Validate device_id to prevent path traversal in directory creation\n    if not device_id or not re.match(r'^[a-zA-Z0-9_\\-]+$', device_id):\n        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400\n\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\n    # Use a safe, unique filename instead of user-controlled filename\n    safe_filename = f'{uuid.uuid4().hex}.tar.gz'\n    archive_path = os.path.join('/tmp', safe_filename)\n    firmware_archive.save(archive_path)\n\n    try:\n        with tarfile.open(archive_path, 'r:gz') as tar_pkg:\n            # Validate all members before extraction\n            safe_members = _validate_tar_members(tar_pkg, deployment_path)\n            tar_pkg.extractall(path=deployment_path, members=safe_members)\n    except (ValueError, tarfile.TarError) as e:\n        os.remove(archive_path)\n        return jsonify({'status': 'error', 'message': str(e)}), 400\n    finally:\n        if os.path.exists(archive_path):\n            os.remove(archive_path)\n\n    return jsonify({'status': 'deployed', 'device': device_id, 'location': deployment_path})"}