LangChain 昨天悄悄打了个安全补丁,你的 Agent 可能正在被“越狱“

张开发
2026/6/4 17:33:31 15 分钟阅读
LangChain 昨天悄悄打了个安全补丁,你的 Agent 可能正在被“越狱“
2026 年 4 月 8 日一个普通的周三LangChain-core 一天之内发了两个版本0.3.84 和 1.2.28。大部分人刷到 changelog 的时候会直接跳过——又是修 bug 的小版本。但这次不一样。两个版本有一条一模一样的改动sanitize prompts。CVE 编号 2026-4539。这是安全补丁。说明在此之前LangChain 的 PromptTemplate 存在一个真实可利用的注入漏洞。你可能已经在线上跑着用了 PromptTemplate 的 Agent。一、问题出在哪两行源码的故事先说结论再展开原理。LangChain 的 PromptTemplate 用的是 Python 的str.format_map做变量替换。这个方法有个特点它不区分{}是你写在模板里的还是用户输入带进来的。只要字符串里有{变量名}它就会尝试替换。类比一下这就像餐厅有个规定单据里括号里的都是厨师指令结果顾客在菜名里写了个括号厨师就真的按顾客的意思操作了。来看 LangChain-core 0.3.83补丁之前的核心格式化逻辑# langchain_core/prompts/prompt.py补丁前简化版 class PromptTemplate(StringPromptTemplate): def format(self, **kwargs: Any) - str: kwargs self._merge_partial_and_user_variables(**kwargs) return DEFAULT_FORMATTER_MAPPING[self.template_format]( self.template, **kwargs ) # DEFAULT_FORMATTER_MAPPING[f-string] 最终调用 def format(self, template: str, **kwargs: Any) - str: return template.format_map(kwargs) # ← 问题就在这里就这一行template.format_map(kwargs)。用户传进来的值没有任何处理直接喂给了format_map。二、攻击场景还原从读取变量到泄露 API Key设定你做了一个翻译服务模板长这样from langchain_core.prompts import PromptTemplate template 你是一个专业翻译请将原文翻译成英文。 规则{rules} 原文{user_text} prompt PromptTemplate( input_variables[rules, user_text], templatetemplate )正常请求user_text你好世界一切正常。攻击者请求user_text{rules}执行template.format_map({rules: 保持专业语气, user_text: {rules}})时发生了两轮替换• 第一轮{user_text}→{rules}用户注入的字符串• 第二轮{rules}→保持专业语气原文那栏显示了 rules 的值。这还只是轻量版。更危险的变体# Python 的 str.format_map 支持属性访问和下标访问 user_text {config.OPENAI_API_KEY} # 如果你无意中把配置对象传进了 kwargs线上代码里不罕见 result prompt.format( rules保持专业语气, user_textuser_text, configapp_config # ← 假设在某个中间件里统一注入了上下文 ) # 输出里会包含你的 OPENAI_API_KEY print(result) # 你是一个专业翻译... # 规则保持专业语气 # 原文sk-proj-xxxxxxxxxxxxxxxxxx str: 把用户输入里的 { 和 } 转义阻止二次解析 return value.replace({, {{).replace(}, }}) def format(self, **kwargs: Any) - str: kwargs self._merge_partial_and_user_variables(**kwargs) if self.template_format f-string: sanitized { k: _sanitize_value_for_fstring(str(v)) if isinstance(v, str) else v for k, v in kwargs.items() } return DEFAULT_FORMATTER_MAPPING[f-string](self.template, **sanitized) # jinja2 路径启用 SandboxedEnvironment...把{替换成{{。在 Python f-string 语法里{{是转义字符最终渲染成字面量{不会被当占位符解析。和 SQL 参数化查询的哲学完全一致数据和结构分离不让数据改变结构的语义。Jinja2 路径启用沙箱from jinja2.sandbox import SandboxedEnvironment def jinja2_formatter_safe(template: str, **kwargs: Any) - str: env SandboxedEnvironment() # 限制属性访问禁止危险方法 return env.from_string(template).render(**kwargs)SandboxedEnvironment会阻断通过__class__.__mro__等 dunder 属性爬升到系统类的路径大幅收窄 SSTI 的可利用面。五、你的代码要怎么改四个可直接执行的步骤步骤一马上升级pip install --upgrade langchain-core # 目标 0.3.84v1.x 用户升到 1.2.28 # 验证版本 python3 -c import langchain_core; print(langchain_core.__version__)步骤二全局搜索高风险用法# 找所有用了 jinja2 格式的地方 grep -r template_format.*jinja2 --include*.py . # 找所有往 PromptTemplate.format() 传参的调用 grep -r \.format( --include*.py . | grep -v ^Binary重点关注变量值来自用户请求、数据库查询结果、第三方 API 返回值的地方。步骤三纵深防御——额外的输入过滤层升级之后 LangChain 自己会做 sanitize但多一层纵深防御没坏处import re import logging def validate_prompt_input(text: str, max_length: int 2000) - str: 在用户输入进入 PromptTemplate 之前的额外校验层 升级到 0.3.84 后保留为纵深防御 if len(text) max_length: raise ValueError(f输入超长{len(text)} {max_length}) # 检测可疑的模板语法记录日志供安全审计 if re.search(r\{[^}]{0,100}\}, text): logging.warning(f[SECURITY] 检测到可疑模板语法来源IP可能需要关注{text[:100]!r}) return text # 用法 user_input validate_prompt_input(request.get(user_text, )) result prompt.format(rules保持专业语气, user_textuser_input)步骤四模板结构绝对不能来自用户输入# 危险让用户选择模板字符串 user_template request.get(template) prompt PromptTemplate(templateuser_template, ...) # ← 绝对不行 # 安全用白名单选预定义模板 TEMPLATE_REGISTRY { translate: 请将以下文本翻译成英文{text}, summarize: 请总结以下内容不超过200字{text}, qa: 基于以下上下文回答问题\n上下文{context}\n问题{question}, } template_key request.get(template_type) if template_key not in TEMPLATE_REGISTRY: raise ValueError(f不支持的模板类型{template_key}) prompt PromptTemplate.from_template(TEMPLATE_REGISTRY[template_key])六、这个漏洞揭示了一个更大的问题从工程角度看这个漏洞背后有个 AI 应用特有的系统性风险比漏洞本身更值得关注。传统 Web 应用的数据流相对简单用户输入 → 验证 → 业务逻辑 → 数据库/响应。AI 应用的数据流复杂得多而且有个关键区别中间环节的数据会被重新注入到下一轮的 Prompt 里。用户输入↓向量数据库检索结果来自外部文档可能被攻击者预先污染↓注入面 → PromptTemplate.format()补丁前↓LLM 推理 → Agent 决策↓工具调用返回值再次进入下一轮 Prompt↓注入面再次放大 → PromptTemplate.format()多轮迭代↓最终输出 / 副作用发邮件、写文件、调外部接口任何中间节点——工具返回值、RAG 检索结果——都可能携带恶意的模板语法。而且 Agent 的多轮迭代会把注入面放大。这意味着 AI 应用的安全模型需要更深层地思考什么是可信数据。不仅用户输入不可信外部工具的返回值同样不可信。本周 MCP Python SDK v1.27.0 发布也包含安全修复——通过 MCP 工具返回的内容如果被不加过滤地塞进 Prompt同样是注入面。这不是 LangChain 一家的问题是整个 AI 工具链的系统性风险。小结把这次漏洞的核心逻辑串一遍•根因str.format_map不区分模板变量和变量值里的{}用户输入可触发二次模板解析•影响读取其他变量值在有敏感对象传入时泄露配置数据jinja2 模式下执行任意代码•修法变量值进入格式化之前把{}转义成{{}}jinja2 启用 SandboxedEnvironment•原则模板结构不能来自用户输入所有进入 Prompt 的外部数据都视为不可信接下来值得关注的方向AI 应用的输入信任边界到底在哪里Framework 层LangChain 做、工具调用层开发者做、LLM 的 system prompt 层模型层做——理想状态是三层都有但目前缺乏统一的标准和最佳实践。这块会是 2026 年 AI 安全方向的热点。如果这篇文章对你有帮助欢迎转发给正在用 LangChain 做 AI 应用的同事。

更多文章