Check Point Research 在 2026 年 6 月发了一篇安全分析:From SQLi to RCE – Exploiting LangGraph's Checkpointer。研究对象是 LangGraph——LangChain 生态里用来构建「有状态、多步 Agent」的开源框架,PyPI 月下载量超过 5000 万。
文章核心结论并不复杂:Agent 需要记忆,记忆落在 Checkpointer 里;Checkpointer 若把用户输入拼进 SQL、再用不安全的方式反序列化 checkpoint,整条链可以打到 RCE。
下面是我读完原文后的重排解读,不是翻译稿。
一分钟版
| 项目 | 内容 |
|---|---|
| 影响组件 | LangGraph 的 SQLite / Redis Checkpointer(持久化层) |
| 关键 CVE | CVE-2025-67644(SQLite SQL 注入)、CVE-2026-28277(msgpack 不安全反序列化)、CVE-2026-27022(Redis 同类注入) |
| 攻击链 | 用户可控 filter → SQL 注入写入假 checkpoint → 反序列化恶意 msgpack → 执行任意命令 |
| 谁危险 | 自托管 LangGraph、使用 SQLite/Redis Checkpointer,且把 get_state_history(filter=...) 暴露给不可信输入的应用 |
| 谁相对安全 | LangChain 托管的 LangSmith Deployment(原 LangGraph Platform),跑 PostgreSQL,不在此次影响面内 |
| 怎么修 | 升级到 langgraph-checkpoint-sqlite 3.0.1+、langgraph 1.0.10+、langgraph-checkpoint-redis 1.0.2+、langgraph-checkpoint 4.0.1+ |
Checkpointer 是什么
LangGraph 把 Agent 的执行过程切成多步,每一步的状态(对话、工具结果、中间决策)需要存下来,以便:
- 断点续跑
- 回溯历史
- 多 Agent 协作时共享上下文
Checkpointer 就是这套「记忆层」。常见实现包括 SQLite、PostgreSQL、Redis。SQLite 里有一张 checkpoints 表,其中 metadata 存 JSON 上下文(如 user_id、step),checkpoint 存序列化后的状态 blob。
问题出在:读这些记忆时,框架假设调用方是可信的——但生产里 filter 往往来自 API 参数。
漏洞一:metadata 过滤里的 SQL 注入(CVE-2025-67644)
触发路径
应用若调用 get_state_history(filter=用户输入),内部会走到 Checkpointer 的 list(),用 filter 字典按 metadata 字段查历史 checkpoint。
框架把 filter 的 key 拼进 SQL 的 JSON 路径:
f"json_extract(CAST(metadata AS TEXT), '$.{query_key}') {operator}"query_key 若含单引号,可逃逸 JSON 路径字符串,注入任意 SQL——典型 SQL 注入。
为什么 SQL 注入还不够「致命」
注入本身只能改查询;真正危险的是:list() 查出来的每一行都会把 checkpoint 列交给反序列化逻辑:
self.serde.loads_typed((type, checkpoint))
也就是说:谁能控制查询结果里的 (type, checkpoint) 元组,谁就控制了反序列化输入。
攻击者用 UNION SELECT 塞一行假数据:type='msgpack',checkpoint 为精心构造的二进制 payload。查询照常返回,框架照常反序列化——SQL 注入成了「投毒入口」。
漏洞二:msgpack 扩展类型导致 RCE(CVE-2026-28277)
LangGraph 用 msgpack 存 checkpoint。反序列化时注册了自定义 ext_hook,其中 EXT_CONSTRUCTOR_SINGLE_ARG 的处理逻辑等价于:
# 简化理解:从 payload 解出 (module, name, arg) 三元组 return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
攻击者构造 msgpack,让三元组变成 ("os", "system", "恶意命令"),执行路径就是:
import os- 取
os.system os.system("攻击者命令")
任意代码执行。 Pickle 默认关闭;JSON 路径在 Check Point 同期研究(LangGrinch)里未直接导致 RCE;msgpack + 自定义 ext_hook 是这条链的终点。
漏洞三:Redis Checkpointer 同类问题(CVE-2026-27022)
Redis 实现里,filter 的 key 同样被直接插值进查询而非参数绑定,注入类与 SQLite 版同构。前提一致:应用暴露带用户可控 filter 的 get_state_history(),且使用 Redis Checkpointer。
完整攻击链(四步)
用户请求带恶意 filter
↓
SQL 注入 → UNION 注入假 checkpoint 行(type=msgpack, blob=恶意)
↓
list() 遍历结果 → loads_typed() 反序列化
↓
ext_hook 执行 os.system(…) → RCE入口通常是 get_state_history() 的 filter 参数未校验。这在「给用户提供按 metadata 筛历史会话」的产品功能里并不罕见——例如按 user_id、session_id 过滤——若 key 或结构来自前端/URL,风险就成立了。
谁该紧张,谁可以松口气
需要排查:
- 自托管 LangGraph,Checkpointer 用 SQLite 或 Redis
- 代码或 API 把
filter字典(尤其 key) 直接传给get_state_history()/checkpointer.list() - 服务对 不可信用户 开放(多租户 SaaS、公网 Agent 面板等)
相对不在影响面:
- 只用 LangChain LangSmith Deployment 托管、后端 PostgreSQL 的团队(官方说明该路径不受此次 SQLite/Redis 问题影响)
额外发现: 研究还指出 SQLite/PostgreSQL Checkpointer 里 LIMIT、ttl 等整型参数存在拼接式 SQL(Python 类型提示运行时不强制),LangChain 团队在披露过程中一并修了——属于纵深防御,说明持久化层曾有多处「字符串拼 SQL」的习惯。
升级清单
| 包 | 建议版本 |
|---|---|
langgraph-checkpoint-sqlite | 3.0.1+(CVE-2025-67644) |
langgraph-checkpoint-redis | 1.0.2+(CVE-2026-27022) |
langgraph-checkpoint | 4.0.1+(CVE-2026-28277) |
langgraph | 1.0.10+ |
披露时间线:2025-11 报给 LangChain;SQLite 修复 2025-12 公开;Redis 2026-02;msgpack 2026-03。社区在 2025 年 11–12 月也有独立研究触及相同 CVE,细节见 LangChain 安全公告。
对写 Agent 应用的人意味着什么
- 持久化层是信任边界。 Agent「记忆」和 Web 应用「数据库」一样,不能默认只有开发者会调用。
- 不要把用户输入拼进 SQL 路径。
filter的 key 必须是白名单字段,value 必须参数化——不能学框架早期_metadata_predicate的做法。 - 反序列化格式要有态度。 自定义 msgpack ext 若等价于「动态 import + 调用任意函数」,等于内置后门;生产应用应限制可反序列化类型,或不用可执行语义的扩展。
- 自托管 ≠ 免运维。 框架下载量大,不等于默认配置适合公网;Checkpointer、工具执行、文件读写要一起纳入威胁模型。
若你在本博客 AI Agent 系列 里跟过 LangChain 本地 Agent 教程,这次漏洞正好提醒:能跑起来的 Demo 和能上线的服务之间,差的就是这类持久化与输入边界。
参考
- Check Point Research 原文
- LangChain 安全公告(CVE-2025-67644、CVE-2026-28277、CVE-2026-27022)
本文为技术解读,不构成安全审计结论。生产环境请对照官方 advisory 与依赖树自行验证版本。