找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5180

积分

0

好友

704

主题
发表于 1 小时前 | 查看: 4| 回复: 0

漏洞概览

Chainlit 是一个开源的 Python 框架,专为快速构建对话式 AI 应用与 LLM 接口而生。它底层基于 FastAPI 和 Socket.IO,提供丰富的 UI 组件和实时通信能力,让开发者能轻松打造类似 ChatGPT 的交互界面。该框架在聊天机器人、AI 助手、客服系统等场景中应用广泛,支持 OpenAI、Claude、LangChain 等多种模型后端,并集成了用户认证、会话管理、文件处理等企业级功能。

这个漏洞的核心问题在于:Chainlit 处理自定义元素(Custom Element)时,对用户传入的文件路径未做任何校验,也未有效拦截未认证用户。这使得攻击者可令服务器读取本地任意文件,并通过接口取回内容。

确切地说,漏洞由两个独立代码缺陷叠加而成:

  • 缺陷一:权限检查形同虚设。当服务器未配置强制身份认证时,核心接口中 if current_user 的判断会被直接跳过,流程一路放行。
  • 缺陷二:路径校验完全缺失。用户传入的 path 字段被原封不动地塞进文件读取函数,攻击者可以指向服务器上的任意位置。

这两个缺陷单拿出来都已足够严重,组合在一起则形成了“无需认证 + 任意文件读取”的高危漏洞。

一张手绘风格的漏洞分析图,标题为“漏洞根因:双重缺陷叠加”。左侧红框标注权限检查可绕过缺陷,右侧黄框标注路径校验缺失缺陷,底部紫框为组合形成的未授权任意文件读取后果。

漏洞复现

整个攻击过程仅需两步,总共两次 HTTP 请求外加一个 WebSocket 连接:

  1. /project/element 发送 PUT 请求,在请求体中注入包含任意文件路径(如 /etc/passwd)的 path 字段。服务器会读取该文件并缓存,同时通过 WebSocket 返回一个“文件令牌”,也就是 chainlitKey
  2. 携带这个文件令牌访问 /project/file/{chainlitKey},服务器直接将已缓存的文件内容返回给攻击者。

手绘风格的攻击流程图,标题为“CVE-2026-22218 攻击流程”。上半部分为第一步路径注入与令牌获取,下半部分为第二步利用文件令牌读取敏感内容。

首先创建一个 demo.py 文件,搭建存在漏洞的环境:

import chainlit as cl

@cl.step(type="tool")
async def tool():
    # Fake tool
    await cl.sleep(2)
    return "Response from the tool!"

@cl.on_message  # this function will be called every time a user inputs a message in the UI
async def main(message: cl.Message):
    """
    This function is called every time a user inputs a message in the UI.
    It sends back an intermediate response from the tool, followed by the final answer.

    Args:
        message: The user's message.

    Returns:
        None.
    """

    # Call the tool
    tool_res = await tool()

    await cl.Message(content=tool_res).send()

利用 Python 虚拟环境快速搭建测试环境:

python -m venv venv
venv\Scripts\Activate.ps1
python -m pip install chainlit==2.9.3 #安装存在漏洞的 chainlit 版本
chainlit run demo.py -w

深色背景网页界面,中央为 Chainlit 品牌标志与输入框,地址栏显示 localhost:8000。

运行构造好的 chainlit_file_read_exploit.py,指定目标 URL 和要读取的文件路径,便能直接将文件内容打印出来。

命令行界面截图,显示漏洞利用工具成功读取目标系统 hosts 文件内容。

漏洞分析

第一步:路径注入与令牌获取

攻击者调用 PUT /project/element 接口注入恶意文件路径。当请求参数中包含指向任意文件的 path 字段时(例如 /etc/passwd),服务器端的 persist_file() 函数会读取该文件,将其复制到临时目录,并将临时路径与一个随机生成的文件 ID 建立映射,存入当前会话的映射表 session.files 中。随后,服务器通过 WebSocket 消息将这个文件 ID(即 chainlitKey)推送给客户端。攻击者只需监听 WebSocket 连接就能捕获到这个关键标识。

我们先来看 server.py 中的 update_thread_element 函数:

代码编辑器截图,显示 update_thread_element 函数源码,其中包含基于 current_user 的条件判断。

update_thread_element 函数在收到请求后会调用 Element.from_dict() 解析请求体中的元素字典。该方法会根据 type 字段判断元素类型,当 typecustom 时就创建 CustomElement 对象,之后服务器就会调用这个对象的 update() 方法。

接着看 element.py 中的 from_dict 方法:

代码编辑器截图,显示 Element 类的 from_dict 方法,代码逻辑中提取了 path、url 等多个字段。

在对象创建过程中,from_dict() 会一股脑提取请求中的所有字段,包括用户可控的 path 字段,并将其传递给对象构造函数。这时候,恶意路径(如 /etc/passwd)就被完整保存到 CustomElement 对象的 path 属性中了。

之后调用 CustomElementupdate() 方法,其内部会调用父类 Elementsend() 方法:

代码编辑器截图,显示 CustomElement 类的 update 方法实现。

send() 方法做的第一件事,就是调用 await self._create(persist=persist) 来执行文件持久化处理:

代码编辑器截图,显示 Element 类的 send 方法,关键调用为 self._create。

_create() 方法检测到对象中存有 path 属性后,便会调用 session.persist_file() 函数:

代码编辑器截图,显示 Element 类的 _create 方法实现。

session.persist_file() 函数使用 aiofiles 异步读取攻击者所指定路径的文件,将其内容复制到会话专属的临时目录下(如 /tmp/chainlit/{session_id}/{file_id})。同时,它生成一个随机的 UUID 格式文件 ID,并在 session.files 映射表中建立 ID 与临时文件路径的关联:

代码编辑器截图,显示 BaseSession 类的 persist_file 方法,使用了 aiofiles 进行异步文件复制。

文件持久化完成后,send() 方法执行第二个关键操作:调用 await context.emitter.send_element(self.to_dict()) 将元素信息发送到前端。

to_dict() 方法负责将 CustomElement 对象转换为字典格式,该字典包含了对象所有关键属性,包括 idtypenamedisplay 以及至关重要的 chainlitKey(即刚获得的文件 ID):

代码编辑器截图,显示 Element 类的 to_dict 方法,构建的字典中包含 chainlitKey。

转换后的字典会通过 send_element() 传递给 emitteremit 函数:

代码编辑器截图,显示 send_element 方法的实现,核心是调用 self.emit。

这个 emit 函数是在建立 WebSocket 连接时注入到会话对象中的闭包。它调用 Socket.IO 的全局发送方法,将包含 chainlitKey 的元素字典通过 WebSocket 推送给客户端。攻击者监听 WebSocket 消息流,捕获事件名为 element 的消息,从中提取 chainlitKey 字段的值,即可获得文件 ID。至此,第一步的路径注入、文件复制和令牌获取就全部完成了。

第二步:读取任意文件

拿到文件 ID 之后,攻击者便可访问 GET /project/file/{file_id} 接口读取文件内容。

server.py 中的 get_file 函数长这样:

代码编辑器截图,显示 get_file 函数的实现,包含权限校验和文件查找逻辑。

服务器根据请求中的 session_id 参数定位到对应会话,再从会话的 files 映射表中查找该 ID 对应的临时文件路径。由于存在 if current_user: 的逻辑缺陷,未认证用户可以轻易绕过权限验证。服务器直接使用 FileResponse 返回临时文件的内容,而这个临时文件已经是目标敏感文件的完整副本。就这样,无需任何身份认证,攻击者仅靠一个匿名 WebSocket 连接就实现了任意文件读取。

漏洞修复

官方已经在 2.9.4 及以上版本中修复了此漏洞,强烈建议尽快升级。如果想进一步了解此类 Web 应用常见的安全/渗透/逆向风险及防御思路,可以参考云栈社区的相关专题。

修复的核心是引入了 _sanitize_custom_element() 输入清理函数,用白名单机制重构了 update_thread_element()delete_thread_element() 两个接口的元素处理逻辑。

代码编辑器截图,显示修复后的 update_thread_element 函数,改用 _sanitize_custom_element 处理输入。

代码编辑器截图,显示 _sanitize_custom_element 函数实现,仅提取 id、name、props 等安全字段。

现在,在创建 CustomElement 对象时,仅会提取并验证 idnamedisplayprops 等合法字段。用户可控的 path 字段在对象构造阶段就被彻底丢弃。即使攻击者在请求中注入了恶意的路径遍历值,也无法传递到后续的文件操作流程中。

一张对比说明图,标题为“官方修复:白名单过滤”。左侧红框为漏洞版本,提取全部字段;右侧绿框为修复版本,只保留白名单字段,path 被过滤。

这一改动从根本上切断了通过 /project/element 注入路径,再通过 /project/file/{chainlitKey} 读取文件的攻击链,有效阻止了利用。




上一篇:告别臃肿与卡顿:TablePro,Mac 原生数据库客户端上手指南
下一篇:郝建业:补齐记忆、安全与Harness框架,智能体才能真落地
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-7 06:53 , Processed in 0.625944 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表