Why ToLO Matters
这一页回答两个问题:ToLO 是什么 和 为什么单独给它命名。
如果你已经读完 背景与问题定位,概念位置已经定好,这里把它展开到可以用一段代码验证的程度。
一句话定义(精确版)
ToLO(Trust-on-LLM-Output,信任 LLM 输出)指开发者把 LLM(large language model,大语言模型)推理 API 的返回字段错误地纳入可信数据域。当 AIMessage.content、tool_call.arguments、retrieved_doc.metadata、OutputParser 解析结果、structured output 字段等在框架代码中作为程序内部数据继续传播,并最终进入反序列化、eval、SQL 拼接、shell 调用、模板渲染、文件路径或 URL 等危险 sink(敏感操作点)时,就出现 ToLO。
这个定义有三个关键词:
- 错误纳入(misplaced):开发者意图把它当内部数据,但攻击者实际可影响它。
- 可信数据域(trusted domain):区别于 HTTP 参数、上传文件、外部 API 响应这些”默认不可信”的输入。
- 既有 sink(known dangerous operations):不发明新 sink,复用已有 CWE 体系。
换成污点分析语言,ToLO 是:
S_LLM source ──► known dangerous sink 没有类型匹配的 C_SAFE sanitizer = 信任模型失效其中 S_LLM 是 LLM-related source family,也就是 LLM 直接产生、被 LLM 框架包装、由 parser/structured-output 派生,或经 RAG(retrieval-augmented generation,检索增强生成)链路进入应用的可攻击者影响数据。C_SAFE 是 ToLO-aware sanitizer family,指真的能收窄下游语义的防护,包括 schema、allowlist、parameterized、safe-codec、capability 五类。
ToLO 的 source 到底长什么样
LLM 输出在不同框架里被包装成不同对象。下面是一张速查表,所有这些都属于 S_LLM。注意 S_LLM 不只包括”模型刚吐出的自然语言字符串”,也包括经框架对象、parser、structured output 或 RAG 抽象继续传播的派生数据。
| 框架 / SDK | 字段路径 | 子集 |
|---|---|---|
| OpenAI SDK | response.choices[0].message.content | S_LLM^{direct} |
| OpenAI SDK | response.choices[0].message.tool_calls[i].function.arguments (JSON 字符串) | S_LLM^{direct} |
| Anthropic SDK | response.content[i].text | S_LLM^{direct} |
| Anthropic SDK | response.content[i].input (tool use block) | S_LLM^{direct} |
| LangChain | AIMessage.content | S_LLM^{framework} |
| LangChain | AIMessage.tool_calls[i]["args"] | S_LLM^{framework} |
| LangChain | AgentAction.tool_input | S_LLM^{framework} |
| LangChain | PydanticOutputParser 输出字段 | S_LLM^{parsed} 或 S_LLM^{structured} |
| LlamaIndex | Response.response (str) | S_LLM^{framework} |
| LlamaIndex | NodeWithScore.node.text (从向量库检索的片段) | S_LLM^{rag} |
| OpenAI structured output | response_format=YourModel 实例字段 | S_LLM^{structured} |
| Anthropic Tool Use | tool_use.input | S_LLM^{structured} |
| MCP | tools/call 返回的 content[].text | S_LLM^{framework} (从 MCP server 回流) |
| 任意 framework | Document.page_content / Document.metadata | S_LLM^{rag} |
它们看起来都像”框架内部对象的字段”。这正是 ToLO 的隐蔽性所在 —— 开发者读代码时眼神扫过这些字段,容易当成”程序内部值”处理。
攻击者怎样影响 S_LLM
ToLO 不把 prompt injection 设为唯一前提。只要攻击者能影响进入框架的 LLM-related source,数据流后半段的信任问题就成立。本站用 C1-C5 表示五类影响通道:
| 通道 | 含义 | 与 ToLO 的关系 |
|---|---|---|
C1 direct prompt injection | 攻击者直接给应用发 prompt | 最直观的触发器,但不是唯一触发器。 |
C2 indirect prompt injection | 攻击者控制的网页、文档或消息被放进上下文 | 输出看似由应用检索得到,实则被外部内容诱导。 |
C3 RAG poisoning | 检索索引中存在攻击者控制或污染的条目 | 使 Document.page_content / metadata 等 S_LLM^{rag} 受影响。 |
C4 tool response control | 攻击者控制某个工具、MCP server、搜索结果或插件响应 | 工具返回内容回流给模型或 agent 状态,再影响下一步动作。 |
C5 model supply-chain compromise | 上游模型权重、fine-tuned 模型或兼容 endpoint 被污染/替换 | 不依赖 prompt 成功率,输出本身可能已偏离预期。 |
这也是为什么”我们已经做了 prompt injection 过滤”不是 ToLO 的充分修复。它可能降低 C1/C2 概率,但没有改变 S_LLM 到 sink 的信任边界。
为什么”看起来像内部数据”是个陷阱
举个对比。看下面两段几乎一样的代码:
# 代码 A:HTTP 后端@app.route("/run")def run(): expr = request.args["expr"] # ← HTTP 参数,任何开发者都会警惕 return str(eval(expr)) # 这里所有人都看得出是 RCE# 代码 B:LLM 后端def run(question: str): resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": question}], ) expr = resp.choices[0].message.content # ← 看起来像"程序内部产出" return str(eval(expr)) # 实际上和代码 A 一样危险两段代码的危险性完全一样:eval 都接收攻击者可影响的字符串。但开发者评审时,对代码 A 会立刻警铃大作,对代码 B 往往会”我们让模型生成的,模型不会乱写”地放过。
ToLO 研究的是怎么把代码 B 的 source 标记和代码 A 一样醒目。
这也是 ToLO 的 source 端新意:不是发现 eval 危险,而是指出 resp.choices[0].message.content 应该和 request.args["expr"] 一样进入 untrusted source 集合。
它是什么,不是什么
ToLO 不是某一具体漏洞,而是一种信任模型失效(trust model failure)—— 开发者把攻击者可影响的数据错误归入可信域。这种 misplacement 与不同下游 sink 结合后,分化为反序列化注入、命令注入、SQL 注入、SSRF、路径穿越、模板注入等具体 CWE 实例。
因此一个 ToLO 案例不需要发明新的危险 API。危险 API 早就被安全社区认识:exec 危险、shell=True 危险、拼接 SQL 危险、不安全反序列化危险。ToLO 的新意在于,触达这些 API 的值不再是传统意义上的 HTTP 参数或上传文件,而是被框架层层包装后的 LLM 输出。
ToLO 不与 OWASP LLM05 抢同一抽象层级。OWASP LLM05 是行业告知性的 broad category,回答”是否需要关心 LLM 输出处理”;ToLO 是程序信任层面的 trust model 解释,回答”这一现象的根因是什么、如何机械化检测、生态中分布如何、修复模式是什么”。一个 ToLO 实例必然落在 LLM05 的语义外延内,反之不成立。
ToLO 也不是 prompt injection。Prompt injection 研究”如何让 LLM 输出违背预期内容”;ToLO 研究”框架代码如何处理 LLM 输出”。两者是数据流上的前置与后置:即使 prompt injection 被完美防御,攻击者仍可通过 RAG 投毒、tool 响应控制、模型供应链污染等通道触发 ToLO。
一个简单判断是:如果修复方案只改 prompt、系统消息或模型对齐,却没有改变输出进入 sink 前的验证、参数化、隔离或权限控制,那么它通常没有真正修复 ToLO,只是降低了某些通道的触发概率。
ToLO 也不是 model security / model safety 本身。Model-security 工作关心模型权重、训练数据、越狱、对齐、投毒、推理服务等问题;ToLO 把模型当成一个可能输出攻击者可影响内容的组件,研究应用代码之后怎样处理这些内容。换句话说,ToLO 不评价”模型为什么这样输出”,只评价”程序为什么敢把这个输出当可信内部数据”。
为什么初学者容易漏掉 ToLO
第一,LLM 输出看起来不像用户输入。开发者看到 HTTP 参数时会警惕,但看到 AIMessage、ChatResponse、ToolCall 这种框架对象时,容易把它当成”内部值”。
第二,结构化输出会制造安全错觉。{"action": "run_sql", "query": "..."} 看起来比自然语言更规整,但 "query" 仍然可以是任意字符串。结构化的本质只是改变了 表示形式,没有改变 内容来源。
第三,很多 agent 设计本来就鼓励模型决定下一步动作。为了灵活性,开发者会让模型选择工具、填写参数、生成查询或代码。ToLO 研究的正是这种”模型决定动作”与”程序安全边界”之间的冲突。
第四,审计日志通常看不见 LLM 输出。HTTP 参数有 nginx access log、WAF rule;LLM 输出在多数应用里既不签名也不审计,出事后回放都困难。
为什么是新的研究对象
经典污点分析的 source 是 HTTP 请求、文件读取、命令行参数等显式 untrusted input,SAST 工具已识别多年。ToLO 的 source 是 LLM API 返回字段 —— 这些字段在多数默认规则或常见配置中通常尚未被建模为 source,因为它们看起来是”程序内部产生的数据”。
ToLO 的技术创新点不是新的污点传播算法,而是 source class 的扩展:把 LLM 推理 API 的返回字段形式化为一个新的 untrusted source family。算法机制刻意保持标准,贡献集中在 source 规范本身。
更精确地说,ToLO 要把五类 source 子集纳入同一个 S_LLM:
| 子集 | 说明 | 典型误判 |
|---|---|---|
S_LLM^{direct} | 直接 SDK 返回字段,如 message content 和 tool call arguments | ”这是模型返回的,不是用户输入。“ |
S_LLM^{framework} | 编排框架封装后的 message、agent action、tool input | ”类型已经是框架对象,所以可信。“ |
S_LLM^{parsed} | 从 LLM 字符串解析出的 JSON/YAML/正则捕获结果 | ”解析成功等于验证成功。“ |
S_LLM^{structured} | Pydantic/dataclass/JSON schema 结构化输出字段 | ”通过 schema 就安全。“ |
S_LLM^{rag} | RAG 检索返回的文档内容和 metadata | ”这是知识库内容,不是攻击者输入。” |
这个研究对象有三个工程特征。
- 跨框架重复出现。message、generation、agent action、tool input、structured output 在不同框架里名字不同,但角色相似。这意味着同一套 source 规范可以覆盖大量目标。
- 跨 sink 分化。同一个 source 可以进入 code、SQL、shell、URL、path 等不同 sink。这意味着如果只盯 sink 端做规则,会重复劳动且漏掉新 sink。
- 可机械化检测。一旦 source / sink / sanitizer 定义稳定,就可以用标准数据流分析查找路径。CodeQL
TaintTracking::Configuration、Semgreppattern-sources/pattern-sinks都能直接复用。
简单来说:ToLO 是一个 source-anchored 研究问题,sink 是 open taxonomy,算法是 commodity。
这里的 open taxonomy 指 sink 子类可以随框架 idiom 增长。当前教学站点固定讲七类 ToLO-{Deser, Exec, Shell, SQL, Path, SSRF, Template},是因为它们覆盖已有公开案例与最常见危险操作;它们不是说未来不可能出现新 sink 形态。
与传统污点对照
| 维度 | 传统污点 (Web/Native) | ToLO |
|---|---|---|
| Source 例子 | request.args["x"]、sys.argv[1]、req.body | message.content、tool_call.arguments、Document.page_content |
| 开发者直觉 | ”这是外部输入,要检查" | "这是模型/框架产出,应该可信” |
| SAST 默认覆盖 | 几乎所有商用 SAST 都标 | 几乎都不标(2024-2026 起才陆续支持) |
| 审计可见性 | nginx log / WAF rule | 通常不记录推理 token-level 内容 |
| 签名 / 可验证性 | 客户端无、TLS 仅传输层 | 模型输出无来源签名 |
| Sink | 与 Web 相同 | 复用相同 sink(关键观察) |
| 同字段语义跨度 | 通常单一(整数、字符串) | 同一字段可携带 SQL、shell、URL、code 等多种语义 |
最后一行是关键观察:ToLO 与传统污点共享 sink,差异完全压在 source 端。这就是为什么把 ToLO 称为 source-anchored 问题。
ToLO 的最小判定式
把全章浓缩成一句规则:
LLM-influenceable source ──► dangerous sink 中间无 类型匹配 的 sanitizer = ToLO其中:
- LLM-influenceable 包括通道 C1-C5 任一(详见 Threat Model)。
- dangerous sink 指会影响进程、文件、网络、数据库、凭据或其他用户数据的操作。
- 类型匹配 意味着 guard 必须真的约束该 sink 的语义。
json.loads不是 SQL sanitizer;html.escape不是 shell sanitizer;裸Pydantic str不是路径 sanitizer。
C_SAFE 到底安全在哪里
C_SAFE 不是”任何验证函数”的统称,而是必须改变下游可达行为的约束。五类 sanitizer 的边界如下:
| 类别 | 算 C_SAFE 的情况 | 不算 C_SAFE 的情况 |
|---|---|---|
C_SAFE^{schema} | 字段是 Literal / Enum / constrained type,JSON Schema 禁止额外字段并限制值域 | 裸 str、裸 dict、只检查字段存在 |
C_SAFE^{allowlist} | 工具名、路径、URL scheme、数据库表名在显式允许列表中 | denylist、简单关键词过滤、只删 ../ |
C_SAFE^{parameterized} | SQL prepared statement、subprocess.run([...], shell=False)、URL params 分离 | 把 LLM 生成的整条 SQL 或 shell 命令当字符串执行 |
C_SAFE^{safe-codec} | json.loads 代替 pickle.loads,yaml.safe_load 代替 yaml.load,ast.literal_eval 代替 eval | parser 后结果继续作为代码、路径、SQL 字符串使用 |
C_SAFE^{capability} | 当前 session 明确授权某工具、文件、网络域或数据库操作 | 只要模型说要调用就调用 |
关键点是类型匹配。例如 yaml.safe_load 可以避免不安全 YAML 反序列化,但它不能证明解析出来的 "query" 字段适合执行 SQL;Literal["read_doc"] 可以限制工具名,但不能自动证明 doc_id 属于当前用户。
不安全 vs 更安全:一个对照
不安全路径:
# 模型自由决定参数,应用直接执行output = llm.invoke(question)action = json.loads(output.content)# action == {"tool": "read_file", "path": "../../.env"}
handler = TOOLS[action["tool"]] # 任意 tool 名都接受result = handler(action["path"]) # 任意 path 都接受更安全的路径:
# 收窄动作空间 + 强类型枚举 + capability gateALLOWED_TOOLS = {"read_doc"}ALLOWED_DOCS = {"doc-123": "/srv/notes/doc-123.md", "doc-456": "/srv/notes/doc-456.md"}
class ToolCall(BaseModel): tool: Literal["read_doc"] # C_SAFE^{allowlist} (枚举) doc_id: Literal["doc-123", "doc-456"] # C_SAFE^{allowlist}
action = ToolCall.model_validate(json.loads(output.content))real_path = ALLOWED_DOCS[action.doc_id] # C_SAFE^{capability}:固定映射
# 进一步检查:当前用户是否有权读该文档require_capability(session, "read", action.doc_id)return Path(real_path).read_text()差别不在模型是否聪明,而在应用有没有把模型输出限制在可控的动作空间里。
- 不安全版让模型决定”tool 是哪个、参数是什么字符串”。
- 安全版只让模型决定”在 N 个枚举值里选一个”。攻击者就算完全控制 LLM 输出,影响面也限制在这 N 个值之内。
这种”把控制权从字符串收回到枚举”的模式,在所有 ToLO 子类里都适用。后续 Defensive Patterns 会展开讲。
真实证据基础
已有多个公开 CVE / GHSA 共享同一结构性特征。引用前需重新核验 NVD 与厂商 advisory,本站当前标 pending:
- LangChain
LLMMathChain经 LLM 输出代码到exec(ToLO-Exec,CVE-2023-29374) - LangChain
PALChain数学求解的 Python REPL 路径(ToLO-Exec,CVE-2023-36258) - LangChain
GraphCypherQAChain把 LLM 输出 Cypher 执行(ToLO-SQL,CVE-2024-8309;这里把 Cypher 归入广义 query-language sink) - LangChain
_evaluate_expression接numexpr(ToLO-Exec,CVE-2023-39631) - Vanna AI
vanna.ask可视化路径把 LLM 生成的 Plotly 代码送入执行(ToLO-Exec,CVE-2024-5826;同一产品的 text-to-SQL 路径作为相关背景,不作为本 CVE 主结论)
这些案例目前在 NVD 与 GitHub Advisory 上被分散标注为 CWE-502 / CWE-78 / CWE-89 / CWE-94 等不同条目,但根因都可以用同一个 trust model 失效解释。“现象级聚类但根因被分散归类”说明这里存在一个值得单独命名和形式化的程序信任问题。
详细复盘在 Public Case Studies。
如果不命名 ToLO,工程上会怎样
不命名 ToLO,实践上会发生以下事:
- 修复方向被错位。开发者看到 NVD 上写 CWE-94,直觉是”过滤 eval 参数”,但真正的修复在收窄 LLM 输出能影响的动作空间。
- 检测规则碎片化。命令注入规则、SQL 注入规则、路径穿越规则各自有一套 source/sink,LLM 输出在每套规则里都要单独加,容易漏。
- 报告不可比较。“某项目有 SQL injection” 和 “某项目有 RCE” 听起来不同严重程度,实际可能根因都是同一个 LLM 输出未隔离 —— 但被分散归类后看不出统一模式。
- 教学路径断裂。讲 prompt injection 不能讲清楚怎么修,讲 SQL injection 又解释不了为什么 LLM 应用突然又冒出来 —— 缺一个把它们连起来的中间概念。
ToLO 不是命名癖好,而是让以上四件事各归各位。
读完检查
读完本页,你应该能解释:
- 为什么 ToLO 不是 prompt injection。
- 为什么
AIMessage.content应该被当作 untrusted input。 - 为什么 ToLO 的研究重点是 source class,而不是重新发明 sink。
- 为什么修复 ToLO 不能只靠 prompt 或模型对齐。
- 至少 5 个常见框架字段会落入
S_LLM的某个子集。
下一步阅读
- LLM Application Stack:模型输出会经过哪些组件。
- Core ToLO Patterns:ToLO 按 sink 分化的七子类。
- ToLO 与已有概念边界:澄清与 OWASP LLM05、Prompt Injection、经典 CWE 的关系。