Core ToLO Patterns
ToLO(Trust-on-LLM-Output) 是一种信任模型失效:程序把 LLM 输出或 LLM 可影响的数据当成可信内部数据,继续传给会产生安全后果的操作。它不是单一漏洞,而是同一信任模型失效在不同下游 sink 上分化出的具体形态。本页按 sink 类型列出七个教育性锚点:ToLO-{Deser, Exec, Shell, SQL, Path, SSRF, Template}。每一类都对应一个既有 CWE,但 source 端语义是新的——数据来自 S_LLM。
七个子类的命名是教学和检测上的便利,不表示七个互相独立的漏洞,而是帮助读者把同一条 source family 映射到不同后果:反序列化、代码执行、命令执行、数据库查询、文件访问、网络访问与模板执行。
怎么读这一页
每个子类按统一模板:
- 是什么:对应哪类 sink、典型函数、所属 CWE。
- 为什么对 ToLO 重要:工程动机和 LLM 场景下的特殊性。
- 典型 source shapes:五个
S_LLM子集如何承载这条路径。 - 典型 sink shapes:同一类 sink 在不同 API 下的形态。
- 最小教学骨架:不可直接复现的最简化骨架,用来理解形态。
- 什么不是 sanitizer:常见错配,看起来像防御但其实不切断危险语义。
- 有效
C_SAFEpatterns:用哪类 sanitizer,如何组合。 - 安全版重写(部分子类配):一段教学性的”如果按 ToLO 思路修,大概是这样”代码。
- 案例锚点:已有公开 CVE 链接(标注 verification 状态)。
所有代码片段都是教学骨架,不含 exploit payload 和 PoC 复现指令。
先解释术语
Source 是污点分析里的起点。本站把 S_LLM 定义为 LLM 产生或 LLM 可影响、并经框架抽象流动的数据。它有五个子集:
| 子集 | 含义 |
|---|---|
S_LLM^{direct} | 直接 LLM API 客户端返回字段,如 message.content、tool call arguments。 |
S_LLM^{framework} | LangChain、LlamaIndex、Haystack、AutoGen 等框架包装后的 LLM 输出对象。 |
S_LLM^{parsed} | 从 LLM 字符串经 json.loads、yaml.safe_load、正则捕获等解析出来的派生值。 |
S_LLM^{structured} | 结构化输出、function call、tool use、Pydantic model 或 dataclass 字段。 |
S_LLM^{rag} | RAG 检索器返回的 Document / Node 内容或 metadata。它不是模型生成,但可通过索引投毒等通道影响并被框架当作可信上下文处理。 |
Sink 是数据到达后会产生安全后果的位置。例如:把字符串展示在聊天框里通常不是 sink;把字符串交给 eval、数据库、shell 或文件系统就是 sink。ToLO 的七子类都按 sink 命名,因为后果由 sink 决定:同样是 LLM 输出,进入 eval → 代码执行,进入 open → 文件访问,进入 requests.get → 网络访问。如果对 sink 概念不熟,回去读 先修知识 §5。
Sanitizer 是能切断危险语义的检查、转换或隔离措施。ToLO 使用五类 C_SAFE sanitizer:C_SAFE^{schema}、C_SAFE^{allowlist}、C_SAFE^{parameterized}、C_SAFE^{safe-codec}、C_SAFE^{capability}。它们必须与 sink 类型匹配。
Attacker channel 是攻击者影响 S_LLM 的方式。本站统一使用 C1-C5:C1 direct prompt injection,C2 indirect prompt injection,C3 RAG 索引投毒,C4 tool response control,C5 模型供应链污染。ToLO 不依赖某一种通道必然成功;只要任一通道能影响 source,后续 source-to-sink 路径就需要被防住。
统一判定模板
每条 ToLO 路径都可以拆成四列:
| 列 | 要回答的问题 |
|---|---|
| Source | 哪个 S_LLM^{...} 子集承载攻击者可影响数据? |
| Transform | 数据是否经过 parser、message wrapper、tool argument、agent state、RAG metadata 等传播? |
| Sink | 最终进入哪类危险操作? |
| Guard | 路径上是否存在与该 sink 类型匹配的 C_SAFE^{...}? |
判定原则:
- 如果 source 不属于
S_LLM→ 不是 ToLO(可能是经典 CWE)。 - 如果 sink 不敏感 → 通常只是输出质量或内容安全问题。
- 如果 guard 只是 prompt 约束、裸
str类型、日志审计或黑名单 → 通常不能算 sanitizer。 - 如果 guard 类型匹配且强制执行 → 可能是已缓解路径,但仍要看是否能被绕过。
§1 ToLO-Exec —— LLM 输出当代码执行
是什么
ToLO-Exec 指 S_LLM 数据作为程序代码进入 eval、exec、compile、Python REPL、notebook kernel、code interpreter 或类似代码执行环境。对应 CWE-94(Code Injection)。
这里的”代码”不是 shell 命令,而是宿主语言或解释器语言:Python 表达式、JavaScript 片段、notebook cell、数据分析脚本。常见于:PAL(Program-Aided Language model)、code-interpreter、symbolic-math、Plotly 自动可视化、数据分析助手。
为什么对 ToLO 重要
很多 LLM 应用的核心卖点就是”让模型写代码解决问题”。于是危险路径常常不是偶然 bug,而是产品设计的一部分——开发者本来就希望模型”写一段代码帮我算”。这不是 misuse,是 architectural choice。
这使 ToLO-Exec 的修复不能只靠”过滤坏字符串”。如果系统本来允许模型写任意 Python,防御重点必须转向语言子集收窄、沙箱隔离、能力门控和人工/策略审批。
典型 source shapes
| Source shape | 例子 |
|---|---|
S_LLM^{direct} | message.content 被 prompt 要求输出 Python/JS 表达式。 |
S_LLM^{framework} | LangChain PythonREPLTool、agent action、AutoGen code block。 |
S_LLM^{parsed} | 从 Markdown fenced code block、JSON 字段或正则捕获提取出的代码。 |
S_LLM^{structured} | {"language": "python", "code": "..."} 中的 code 字段。 |
S_LLM^{rag} | 检索文档中的代码片段被 agent 当作可运行 cell。 |
典型 sink shapes
| Sink shape | 风险语义 |
|---|---|
eval(expr) | 执行表达式。 |
exec(code)、compile(code, ...) 后执行 | 执行语句块。 |
| REPL / IPython / Jupyter cell runner | 在长期进程里执行模型代码。 |
| DataFrame query engines with Python fallback | 看似数据查询,实际可触发代码求值。 |
| Plugin/script runner | 将模型输出写成脚本再运行。 |
最小教学骨架
from openai import OpenAIclient = OpenAI()
def answer(question: str) -> str: resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "请给出一段计算用的 Python 表达式作为答复。"}, {"role": "user", "content": question}, ], ) expr = resp.choices[0].message.content # ← S_LLM^{direct} return str(eval(expr)) # ← ToLO-Exec sink四列拆解:
| Source | Transform | Sink | Guard |
|---|---|---|---|
S_LLM^{direct} (message.content) | (无,直接传给 eval) | eval(...) (CWE-94) | 缺失 |
什么不是 sanitizer
| 看起来像防御 | 为什么不算 |
|---|---|
黑名单 import、open、os 等关键字 | Python/JS 语法空间太大,黑名单不可完整。 |
长度限制 len(code) < 100 | 100 字符够写 __import__('os').system(...)。 |
Pydantic(code: str) | 只验证类型,不限制语义。 |
| prompt 中写”只输出安全代码” | 属于通道侧约束,不是 sink 前 sanitizer。 |
| 旧版或未审计 Python sandbox | sandbox 本身必须被当作高风险组件,不能只凭名称认定为 sanitizer。 |
有效 C_SAFE patterns
| Pattern | 用法 |
|---|---|
C_SAFE^{safe-codec} | 把”任意 Python”替换成封闭求值器,如 ast.literal_eval 处理字面量、numexpr.evaluate(expr, global_dict={}, local_dict={}) 处理有限表达式。 |
C_SAFE^{schema} | 让模型选择 action: Literal["sum", "plot"],不要返回自由代码。 |
C_SAFE^{allowlist} | 允许的函数、变量、列名、图表类型均从固定 registry 选择。 |
C_SAFE^{capability} | 如果必须执行代码,放入无网络、只读文件系统、资源限额明确的隔离环境(Docker / gVisor / Firecracker),并按会话授权。 |
案例锚点
| Case | 项目 / 组件 | verification |
|---|---|---|
| CVE-2023-29374 | LangChain LLMMathChain → PythonREPL | pending |
| CVE-2023-36258 | LangChain PALChain(PAL)→ 代码执行 | pending |
| CVE-2023-39631 | LangChain _evaluate_expression → numexpr 历史变体 | pending |
这些锚点在本站保持 pending,除非后续用 NVD、MITRE、GHSA 或项目官方 advisory 核验。
§2 ToLO-Shell —— LLM 输出当 shell 命令
是什么
ToLO-Shell 指 S_LLM 数据进入操作系统命令执行 sink:subprocess.run(..., shell=True)、os.system、os.popen、commands.getoutput、pexpect.run(shell) 或 Bash tool。对应 CWE-78(OS Command Injection)。
它与 ToLO-Exec 的区别:Exec 是宿主语言代码执行;Shell 是把字符串交给 OS shell 或命令行程序解析。常见于:agent 应用的 shell tool、SRE diagnostic agent、CI/CD 助手、自动化运维。
为什么对 ToLO 重要
Agent 类系统经常把 shell 当成通用工具:查日志、运行诊断、操作 repo、调用 CLI、执行部署命令。为了保留灵活性,开发者会有意让 LLM 返回”整条命令”。这种”灵活性”和 shlex.quote 这种保守做法直接冲突。开发者认为”反正是 LLM 选的命令,模型不会乱来”——这正是 ToLO 的核心错误信任。
ToLO-Shell 常常直接触达文件系统、网络、进程和凭据环境变量,因此风险优先级通常很高。
典型 source shapes
| Source shape | 例子 |
|---|---|
S_LLM^{framework} | tool call arguments 中的 cmd、command、args。 |
S_LLM^{structured} | {"command": "git", "arguments": "..."} 中自由参数字段。 |
S_LLM^{parsed} | 从模型文本提取的一行 CLI 命令。 |
S_LLM^{rag} | RAG 文档中的运维命令被 agent 复制执行。 |
S_LLM^{direct} | 模型直接回答”应该运行的命令”。 |
典型 sink shapes
| Sink shape | 风险语义 |
|---|---|
subprocess.run(cmd, shell=True) | shell metacharacter、管道、重定向、命令替换都参与解析。 |
os.system(cmd)、os.popen(cmd) | 等价于交给 shell。 |
subprocess.run(["bash", "-c", cmd]) | 虽然 shell=False,但 bash -c 仍解释字符串。 |
| CLI wrapper 接收完整命令字符串 | 框架隐藏了 shell,但风险语义相同。 |
最小教学骨架
from langchain_core.tools import toolimport subprocess
@tooldef run_command(cmd: str) -> str: """执行一条 shell 命令""" return subprocess.run(cmd, shell=True, capture_output=True).stdout.decode() # ^^^^^^^^^^ ← ToLO-Shell sink,且 shell=True 让攻击面最大四列拆解:
| Source | Transform | Sink | Guard |
|---|---|---|---|
S_LLM^{framework} (tool_call.arguments) | LangChain 自动反序列化 args | subprocess.run(cmd, shell=True) (CWE-78) | 缺失 |
什么不是 sanitizer
| 看起来像防御 | 为什么不算 |
|---|---|
shlex.quote(cmd) 后仍执行整条命令 | quote 不应包住整条自由命令;应拆成固定 executable + 参数列表。 |
过滤 ;、` | 、&` |
cmd.startswith("git ") | 后续参数仍可能改变行为或访问越权目标(git status; rm -rf / 仍以 git 开头)。 |
shell=False 但仍调用 bash -c | 实质仍是 shell 解释。 |
| prompt 中声明”只运行安全命令” | 不是 sink 前防御。 |
有效 C_SAFE patterns
| Pattern | 用法 |
|---|---|
C_SAFE^{schema} | 模型只能选择 command: Literal["status", "list_logs"],而不是写完整 shell。 |
C_SAFE^{allowlist} | executable、flag、目标对象都来自白名单或 registry。 |
C_SAFE^{parameterized} | 使用 subprocess.run([exe, arg1, arg2], shell=False),结构由开发者固定。 |
C_SAFE^{capability} | 会话级工具授权、容器隔离、最小环境变量、无敏感挂载、资源限制、seccomp / capabilities 限制可用 syscall。 |
安全版重写
from typing import Literalfrom pydantic import BaseModelimport subprocess, ipaddress
class DiagnosticAction(BaseModel): command: Literal["ping", "traceroute"] # ← schema 收窄 target_ip: str # ← 仍需校验
ALLOWED_NETWORKS = [ipaddress.ip_network("10.0.0.0/8")]
def run_diagnostic(action: DiagnosticAction) -> str: ip = ipaddress.ip_address(action.target_ip) if not any(ip in net for net in ALLOWED_NETWORKS): raise PermissionError("target out of scope")
return subprocess.run( [action.command, "-c", "3", str(ip)], # ← list 形式,无 shell capture_output=True, timeout=10, ).stdout.decode()四个变化点:
Literal让模型只能”选命令”,不能”写命令”。- 网络段在应用层 allowlist。
subprocess.run用 list 形式,shell=False是默认。- 加
timeout,避免慢命令拖死会话。
案例锚点
参见 Public Case Studies 中的公开列表。本站当前不在本页新增具体 ToLO-Shell CVE 事实;LangChain _run 的 ShellTool 类家族历史上多次被报告,具体 CVE 待核验。
§3 ToLO-SQL —— LLM 输出当数据库查询
是什么
ToLO-SQL 指 S_LLM 数据进入 SQL、Cypher、GraphQL 或类似查询语言执行 sink。对应 CWE-89(SQL Injection)的 sink family,但 text-to-SQL 场景里的 source 和修复方式更特殊。
常见于:text-to-SQL、自然语言 BI 工具、自动 ETL、数据分析 agent。
为什么对 ToLO 重要
自然语言 BI、text-to-SQL、数据分析 agent 的目标就是让 LLM 生成查询。整条 SQL 由 LLM 决定时,传统参数化没有可固定的模板可绑定——execute("... WHERE id = %s", (uid,)) 只在 SQL 结构固定时有效。
因此 ToLO-SQL 的关键防御通常不是”escape LLM SQL”,而是把模型权限收窄为:选择已批准模板、选择表/列白名单、填写参数值,并用数据库账号和行/列级权限兜底。
典型 source shapes
| Source shape | 例子 |
|---|---|
S_LLM^{direct} | 模型直接返回 SELECT ... 字符串。 |
S_LLM^{framework} | SQL agent 的 AgentAction.tool_input。 |
S_LLM^{parsed} | JSON 字段 {"sql": "..."} 或 Markdown code block 中的 query。 |
S_LLM^{structured} | table、column、where、order_by 字段由模型填充。 |
S_LLM^{rag} | RAG metadata 里的表名、列名、filter 条件被拼入查询。 |
典型 sink shapes
| Sink shape | 风险语义 |
|---|---|
cursor.execute(llm_sql) | 整条 SQL 被数据库解析执行。 |
pandas.read_sql(llm_sql, conn) | 读接口仍会执行传入 query。 |
SQLAlchemy text(llm_sql) / session.execute(...) | ORM 不会自动保护 raw SQL。 |
| Cypher / GraphQL raw query runner | 查询语言不同,但 source-to-query 语义相同。 |
最小教学骨架
def text_to_sql(question: str) -> list: resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "把用户问题转成一句 SQL。"}, {"role": "user", "content": question}, ], ) sql = resp.choices[0].message.content # ← S_LLM^{direct} return list(db.execute(sql)) # ← ToLO-SQL sink四列拆解:
| Source | Transform | Sink | Guard |
|---|---|---|---|
S_LLM^{direct} | (无) | db.execute(sql) (CWE-89) | 缺失 |
什么不是 sanitizer
| 看起来像防御 | 为什么不算 |
|---|---|
json.loads 后取 sql 字段 | JSON 只改变载体,字段仍是 tainted SQL。 |
| 关键字黑名单 | SQL 方言、注释、子查询、大小写和编码让黑名单不可靠。 |
| ORM 名称 | session.execute(text(llm_sql)) 仍是 raw query。 |
| 只让模型”生成 SELECT”的 prompt | prompt 不是强制策略。 |
| 普通 escaping | 当结构由 LLM 决定时,escape 不能证明查询语义安全。 |
read_only=True connection 配置 | 比专门的只读账号弱,且不限制读哪些表/列。 |
有效 C_SAFE patterns
| Pattern | 用法 |
|---|---|
C_SAFE^{schema} | 让模型输出查询意图对象,例如 metric: Literal[...]、table: Literal[...]、filters: list[...]。 |
C_SAFE^{allowlist} | 表名、列名、排序字段、聚合函数从固定映射选择,不要直接拼接模型字符串。 |
C_SAFE^{parameterized} | 开发者固定 SQL 模板,模型只提供 value,通过 cursor.execute(template, params) 绑定。 |
C_SAFE^{capability} | LLM-driven 路径连接只读账号(DROP/UPDATE/DELETE 直接被数据库拒绝)、限制 schema、启用行/列级权限、设置 query timeout。 |
安全版重写
from typing import Literalfrom pydantic import BaseModel
# 不让 LLM 自由写 SQL,只让它选模板 + 填参数ALLOWED_TABLES = {"users": "public.users", "orders": "public.orders"}ALLOWED_COLUMNS = { "users": {"id", "email", "created_at"}, "orders": {"id", "user_id", "total"},}
class CountQuery(BaseModel): table: Literal["users", "orders"] where_column: str where_value: str
def run_count(q: CountQuery) -> int: real_table = ALLOWED_TABLES[q.table] if q.where_column not in ALLOWED_COLUMNS[q.table]: raise ValueError("column not allowed")
sql = f"SELECT COUNT(*) FROM {real_table} WHERE {q.where_column} = %s" return db.execute(sql, (q.where_value,)).scalar() # ← 参数化只对 value 部分注意:这里仍有 f-string,但 real_table 来自开发者维护的 allowlist 映射,where_column 也经过白名单校验,where_value 作为参数绑定。开发者负责 SQL 结构,LLM 只填值。
案例锚点
| Case | 项目 / 组件 | verification |
|---|---|---|
| CVE-2024-5826 | Vanna AI text-to-SQL 路径中的 Plotly 代码执行(主路径属 ToLO-Exec) | pending |
| CVE-2024-8309 | LangChain GraphCypherQAChain(Cypher 执行,广义 query-language sink) | pending |
Vanna AI 的案例本站把公开来源支持的主路径写成 ToLO-Exec via Plotly code;text-to-SQL 只作为相关链路背景,不放在本表作为 ToLO-SQL 主锚点。
§4 ToLO-Path —— LLM 输出当文件路径
是什么
ToLO-Path 指 S_LLM 数据作为路径、文件名、目录名或 archive entry name 进入文件系统操作 sink:open / pathlib.Path.read_text|write_text / shutil / os.makedirs / tarfile.extract / zipfile.extract。对应 CWE-22(Path Traversal)及相关文件访问问题。
常见于:文件管理 agent、笔记 / 知识库写入、上传整理工具、log 查看器、RAG ingestion pipeline。
为什么对 ToLO 重要
文件管理 agent 天然需要文件路径。开发者直觉是”模型只会给合理的 name”,不会想到 ../、绝对路径、符号链接。开发者容易把 LLM 选择理解成用户意图,没有把路径当作不可信边界处理。
ToLO-Path 的后果不只是不该读的文件被读;还包括覆盖工作区文件、删除中间产物、写入配置、把越权内容带回 LLM 上下文。
典型 source shapes
| Source shape | 例子 |
|---|---|
S_LLM^{framework} | tool call arguments 中的 path、filename、directory。 |
S_LLM^{structured} | {"doc_id": "...", "target_path": "..."} 的路径字段。 |
S_LLM^{parsed} | 从模型文本提取出的文件名。 |
S_LLM^{rag} | RAG metadata source、path、uri 被用来读原文。 |
S_LLM^{direct} | 模型直接建议要打开或保存的文件路径。 |
典型 sink shapes
| Sink shape | 风险语义 |
|---|---|
open(path)、Path(path).read_text()、write_text() | 读写任意路径。 |
shutil.copy/move/rmtree、os.remove | 移动、覆盖、删除。 |
os.makedirs、archive extract | 创建目录或解压到越权位置。 |
| framework file loader | 看似文档加载,实际接受路径并访问文件系统。 |
最小教学骨架
@tooldef read_note(name: str) -> str: """读取一份笔记""" return open(f"/srv/notes/{name}").read() # ← ToLO-Path (name = "../../etc/passwd")四列拆解:
| Source | Transform | Sink | Guard |
|---|---|---|---|
S_LLM^{framework} (tool_call.arguments["name"]) | f-string 拼接 | open(...) (CWE-22) | 缺失(拼接前没有 Path.resolve() + is_relative_to) |
什么不是 sanitizer
| 看起来像防御 | 为什么不算 |
|---|---|
if ".." in path | 不处理绝对路径、Windows 上 ..\\..\\、符号链接、平台差异、编码等情况。 |
os.path.join(ROOT, name) 不做 resolve 检查 | os.path.join 遇绝对路径会丢弃前面;name="/etc/passwd" 时直接返回 /etc/passwd。 |
使用 pathlib | Path("/srv/notes") / "../../etc/passwd" 等价于 Path("/srv/notes/../../etc/passwd"),必须 resolve。 |
| 文件扩展名检查 | .txt 不证明路径在允许目录内。 |
| prompt 要求”只访问工作区” | 不是 sink 前强制检查。 |
有效 C_SAFE patterns
| Pattern | 用法 |
|---|---|
C_SAFE^{schema} | 让模型输出 doc_id: Literal[...] 或受约束 slug,而不是自由路径。 |
C_SAFE^{allowlist} | root = ROOT.resolve(); target = (root / rel).resolve(); target.is_relative_to(root)。先 resolve 再判断,避免符号链接 / .. 绕过。 |
C_SAFE^{capability} | 进程级 chroot / 容器 mount 隔离 / seccomp 限制文件 syscall;只挂载允许目录,使用只读 mount。 |
C_SAFE^{parameterized} | 对 archive/extract 类 API,固定目标目录并禁止 entry name 决定最终根目录。 |
安全版重写
from pathlib import Pathfrom typing import Literalfrom pydantic import BaseModel
ROOT = Path("/srv/notes").resolve()ALLOWED_DOCS = {"daily-log": "daily-log.md", "todo": "todo.md"}
class ReadNote(BaseModel): doc_id: Literal["daily-log", "todo"] # ← schema 限定枚举
@tooldef read_note(req: ReadNote) -> str: name = ALLOWED_DOCS[req.doc_id] target = (ROOT / name).resolve() if not target.is_relative_to(ROOT): raise PermissionError("path escapes root") # 防符号链接 return target.read_text()三个关键点:
Literal把”路径”收窄成”枚举的 doc_id”。- 应用层
ALLOWED_DOCS字典做 ID → 文件名映射,LLM 不直接给文件名。 resolve()之后用is_relative_to(ROOT)兜底——这一步即使被插入符号链接也能拒绝。
案例锚点
本站当前不在本页新增独立 ToLO-Path CVE 事实。LangChain FileTool 类家族历史上有 path 类报告;具体 CVE 待核验,必须继续标为 pending。
§5 ToLO-SSRF —— LLM 输出当 URL 或网络目标
是什么
ToLO-SSRF 指 S_LLM 数据作为 URL、host、IP、API endpoint 或 webhook 目标进入 outbound request sink:requests.get / requests.post / httpx / urllib.urlopen / aiohttp / urllib3。对应 CWE-918(Server-Side Request Forgery)。
SSRF 的核心是:服务器代替攻击者访问了攻击者指定或影响的网络目标。常见于:web-fetch 工具、URL preview、API caller、外部数据集成 agent、爬虫助手、retriever、browser tool、MCP/tool integration。
为什么 SSRF 在 LLM agent 里特别危险
SSRF 的传统危害是”让服务器访问内网”。在 LLM agent 里还多了一层:服务器拉回的内容会被作为 ToolMessage 写回 LLM 上下文,污染下一轮决策(C2 通道)。这是一种”自我放大”:SSRF 不仅暴露内网,还把暴露内容带回模型推理,继续影响后续 sink 决策。
被 SSRF 拉到的常见目标:
- 云元数据 IMDS:
- AWS:
http://169.254.169.254/latest/meta-data/iam/security-credentials/→ 偷 IAM 临时凭据 - GCP:
http://metadata.google.internal/computeMetadata/v1/ - Azure:
http://169.254.169.254/metadata/instance?api-version=...
- AWS:
- 内网管理 API:
http://internal-admin.svc.cluster.local:8080/admin(K8s 内服务) - 本机 loopback:
http://127.0.0.1:6379/(Redis no-auth)、http://localhost:8500/v1/(Consul)、http://localhost:9200/(Elasticsearch) file://scheme:在urlopen接受 file scheme 时,可以读本地文件(如file:///etc/passwd)- gopher/dict scheme:某些客户端支持,可发起原始 TCP 数据包
因此 ToLO-SSRF 同时影响网络出口、凭据边界、内网服务和后续 S_LLM 污染。
典型 source shapes
| Source shape | 例子 |
|---|---|
S_LLM^{framework} | tool call arguments 中的 url、endpoint。 |
S_LLM^{structured} | structured output 的 scheme、host、path、query 字段。 |
S_LLM^{parsed} | 从模型文本或 JSON 中提取的 URL。 |
S_LLM^{rag} | 文档 metadata 中的 source_url 被自动 fetch。 |
S_LLM^{direct} | 模型直接建议访问某 API 或网页。 |
典型 sink shapes
| Sink shape | 风险语义 |
|---|---|
requests.get(url)、httpx.get(url) | 发起 outbound HTTP 请求。 |
urllib.request.urlopen(url) | 可能支持更多 scheme(file/ftp/gopher),需要特别审查。 |
aiohttp.ClientSession.get(url) | 异步请求同样是 sink。 |
| Browser automation / fetch tool | 通过 headless browser 或 tool server 访问网络。 |
| Generic API caller | LLM 控制 base URL、path、headers 或 method。 |
最小教学骨架
@tooldef fetch_url(url: str) -> str: """抓取 URL 内容""" return requests.get(url, timeout=5).text # ← ToLO-SSRF四列拆解:
| Source | Transform | Sink | Guard |
|---|---|---|---|
S_LLM^{framework} (tool_call.arguments["url"]) | (无) | requests.get(url) (CWE-918) | 缺失 |
什么不是 sanitizer
| 看起来像防御 | 为什么不算 |
|---|---|
只允许 http / https | scheme 正确不代表 host 安全。 |
字符串黑名单 if "169.254" in url: raise | IP 有多种表示法(0x0a.0x00.0x00.0x01 / 十进制 2130706433 / IPv6 mapped),DNS 也可变化。 |
一次 urlparse 就放行 | 解析不等于授权,还要检查 host、port、解析后 IP。 |
| 一次性 host 检查 | DNS rebinding 可在第二次解析时返回不同 IP(TOCTOU)。 |
| 只在 prompt 中禁止内网 URL | 不是 sink 前强制检查。 |
| 只记录访问日志 | 审计不切断请求。 |
有效 C_SAFE patterns
| Pattern | 用法 |
|---|---|
C_SAFE^{schema} | 把 action 固定为 fetch_docs 等少数动作,拆出 host/path/query 字段。 |
C_SAFE^{allowlist} | scheme、host、port、method 均白名单;解析 IP 后拒绝 loopback、private、link-local、metadata 类地址。 |
C_SAFE^{parameterized} | base URL 固定,LLM 只填 query params,不要控制完整 URL。 |
C_SAFE^{capability} | 所有 outbound 走 egress proxy,由 proxy 做最终 allowlist 与 DNS/IP 重检查;DNS rebinding 防御:解析 IP 后再次校验。 |
安全版重写
import requests, ipaddress, socketfrom urllib.parse import urlparse
ALLOWED_HOSTS = {"docs.example.com", "api.example.com"}
def safe_get(url: str) -> str: u = urlparse(url) if u.scheme not in {"http", "https"}: raise ValueError("scheme not allowed") if u.hostname not in ALLOWED_HOSTS: raise ValueError(f"host {u.hostname} not in allowlist")
# 防 DNS rebinding:解析 IP 后再检查 ip = ipaddress.ip_address(socket.gethostbyname(u.hostname)) if ip.is_private or ip.is_loopback or ip.is_link_local: raise ValueError("resolved to internal IP")
return requests.get(url, timeout=5).text三层保护:scheme 白名单、host 白名单、解析后 IP 复核。生产中再叠加 egress proxy 兜底。
案例锚点
本站当前不在本页新增独立 ToLO-SSRF CVE 事实。Web fetcher 或 URL loader 类报告如未核验,必须继续标为 pending。
§6 ToLO-Deser —— LLM 输出进入反序列化
是什么
ToLO-Deser 指 S_LLM 数据进入不安全反序列化 sink:pickle.loads / dill.loads / joblib.load / torch.load(default)/ yaml.load / marshal.loads。对应 CWE-502(Deserialization of Untrusted Data),但 source 不是 HTTP body、cookie 或上传文件,而是被框架当成内部状态、工具参数或对象描述的 LLM 输出。
反序列化(deserialization)是把字节流或字符串恢复为程序对象的过程。问题在于某些 codec 会在恢复对象时触发代码加载、类实例化或特殊方法执行——pickle 可以在 __reduce__ 阶段调用任意可调用对象。
常见于:cache / 持久化中间结果、agent state 读写、自定义模型加载、langchain loads / dumps 历史路径、embedding cache。
为什么对 ToLO 重要
LLM 编排框架经常保存 agent state、tool call、message history、embedding cache、workflow node 或模型输出中间结果。开发者容易把这些数据理解成”框架自己产生的对象”,从而忽略其中字段可能来自 C1-C5 任一攻击者通道。
ToLO-Deser 的风险点不是”开发者不知道 pickle 危险”,而是开发者没有意识到进入 pickle/yaml/模型加载路径的数据已经被 LLM 影响。性能或便利让选择更糟:pickle 序列化任何 Python 对象,yaml 比 JSON “类型更丰富”,torch.load 默认行为是反序列化 pickle。
典型 source shapes
| Source shape | 例子 |
|---|---|
S_LLM^{direct} | 模型直接返回一个被当成 serialized object 的字符串字段(可能 base64 编码)。 |
S_LLM^{framework} | AIMessage.additional_kwargs、agent state、tool result 被框架序列化后再恢复。 |
S_LLM^{parsed} | json.loads(message.content) 得到的 dict 中某字段再被当成 serialized blob。 |
S_LLM^{structured} | Pydantic 字段 state_blob: str、object_spec: dict 被下游恢复成对象。 |
S_LLM^{rag} | RAG metadata 中的对象描述、配置片段、cache key 被后续 loader 当成可信输入。 |
典型 sink shapes
| Sink shape | 风险语义 |
|---|---|
pickle.loads(...)、dill.loads(...)、joblib.load(...) | Python 对象恢复,可能触发任意对象构造语义。 |
yaml.load(..., Loader=...) 中使用 unsafe loader | YAML tag 可映射到复杂对象。 |
torch.load(...) 默认反序列化路径 | 模型/权重加载中可能经过 pickle 语义。 |
marshal.loads(...) | 反序列化 Python code object,可直接执行。 |
framework loads(...) / deserialize(...) | 框架自定义对象恢复,尤其要看是否允许自由 class/type 字段。 |
最小教学骨架
import pickle, base64
def restore_state(llm_blob: str): raw = base64.b64decode(llm_blob) return pickle.loads(raw) # ← ToLO-Deser四列拆解:
| Source | Transform | Sink | Guard |
|---|---|---|---|
S_LLM^{direct} (base64 编码后嵌在响应里) | base64.b64decode | pickle.loads(...) (CWE-502) | 缺失 |
什么不是 sanitizer
| 看起来像防御 | 为什么不算 |
|---|---|
base64.b64decode(...) | 只是编码转换,不改变危险语义。 |
decrypt(blob) 后再 pickle.loads | 加密只保护传输或存储,不能证明 plaintext 不受 LLM 影响;攻击者可通过 prompt injection 影响”签名前内容”。 |
| HMAC 签名 | 只防第三方篡改;如果被签名内容本身来自 LLM,仍然是 tainted。 |
Pydantic blob: str | 只证明字段是字符串,不证明它是安全对象格式。 |
| 长度限制 | 可能降低 DoS 风险,不能切断反序列化语义。 |
restricted_globals / pickle gadget allowlist | Python pickle 反序列化的 gadget chain 历史上多次绕过 restrictions。 |
有效 C_SAFE patterns
| Pattern | 用法 |
|---|---|
C_SAFE^{safe-codec} | 用 json.loads、yaml.safe_load、ast.literal_eval、safetensors 或 torch.load(..., weights_only=True) 替代 unsafe codec。 |
C_SAFE^{schema} | safe codec 之后,用严格 schema 验证字段结构、枚举和取值范围。 |
C_SAFE^{allowlist} | 如果框架必须恢复内部对象,只允许固定 type id 或固定 class registry,不要让 LLM 决定 class path。 |
C_SAFE^{capability} | 对模型加载、文件读取、网络下载等恢复副作用加最小权限和隔离环境。 |
注意:用 safe codec 只解决 ToLO-Deser。解出来的字段如果再进入 SQL、shell、path 或 URL sink,仍需要对应 sanitizer。这是子类组合问题。
案例锚点
LangChain langchain.load.loads / serializable 体系历史上有 deserialization 安全考虑,具体 CVE 待核验。本站案例列表中暂未独立条目;如果未来用官方来源核验,可作为 ToLO-Deser 锚点添加。
§7 ToLO-Template —— LLM 输出当模板字符串
是什么
ToLO-Template 指 S_LLM 数据作为模板字符串本身进入模板引擎:jinja2.Template(...) / Environment.from_string(...) / Django template 直接渲染 / Mako / Handlebars。对应 CWE-1336(Improper Neutralization of Special Elements Used in a Template Engine)。
⚠ 注意区分:LLM 输出作为模板变量值,通常是输出编码 / HTML escaping 问题(那是 sink-side 的 autoescape 问题,不是 ToLO);LLM 输出作为模板代码,才是本节的 ToLO-Template。
为什么对 ToLO 重要
LLM 很适合生成邮件、报告、Markdown、HTML 或 prompt template。开发者可能进一步把这些内容交给模板引擎渲染,希望复用变量替换、循环和条件语法。问题是许多模板引擎不仅做字符串替换,还暴露表达式、filter、函数调用或对象访问能力。
Jinja2 默认环境不沙盒化,Python 内省可触达任意对象 → 历史上多次 SSTI(Server-Side Template Injection)RCE。一旦模型控制模板字符串,应用就把”语言模型生成文本”提升成了”服务器端模板程序”。
典型 source shapes
| Source shape | 例子 |
|---|---|
S_LLM^{direct} | 模型直接返回 HTML/Jinja/Handlebars 模板。 |
S_LLM^{framework} | agent output 被当成 report template。 |
S_LLM^{parsed} | JSON 字段 {"template": "..."}。 |
S_LLM^{structured} | structured output 中的 template_body、subject_template。 |
S_LLM^{rag} | RAG 文档中的模板片段被组合进 server-side template。 |
典型 sink shapes
| Sink shape | 风险语义 |
|---|---|
jinja2.Template(llm_text).render(...) | LLM 控制模板程序。 |
Environment.from_string(llm_text) | 动态模板编译。 |
| Django / Mako / Handlebars server-side render | 具体能力不同,但动态模板字符串均需审查。 |
| Prompt-template compiler | 如果模板语言支持函数/表达式,也可能成为 sink。 |
最小教学骨架
from jinja2 import Template
def render_dynamic(llm_output: str, data: dict) -> str: return Template(llm_output).render(data) # ← ToLO-Template # llm_output = "{{ ''.__class__.__mro__[1].__subclasses__()[...]... }}" # → 模板沙盒逃逸 → RCE四列拆解:
| Source | Transform | Sink | Guard |
|---|---|---|---|
S_LLM^{direct} (作为模板字符串) | Template(...) | .render(...) (CWE-1336) | 缺失 |
什么不是 sanitizer
| 看起来像防御 | 为什么不算 |
|---|---|
autoescape=True | 主要保护变量输出 HTML,不阻止模板字符串被解释。 |
替换 {{ | Jinja 支持 {%- if -%} 等多种语法入口,字符串替换不完整。 |
| Markdown sanitizer | 只处理 HTML/Markdown 输出,不处理 server-side template 执行语义。 |
Pydantic(template: str) | 只验证类型。 |
| prompt 中要求”不要写模板语法” | 不是强制策略。 |
有效 C_SAFE patterns
| Pattern | 用法 |
|---|---|
C_SAFE^{parameterized} | 模板字符串由开发者固定,LLM 只作为变量值传入。 |
C_SAFE^{schema} | 让模型选择 template_id: Literal[...],不要生成模板源码。 |
C_SAFE^{allowlist} | 允许的 filter、component、slot、template id 从 registry 选择。 |
C_SAFE^{capability} | 若必须渲染动态模板,用 jinja2.sandbox.SandboxedEnvironment + 容器隔离 + 无文件/网络能力 + 资源限制,且配合白名单 filter / function。 |
安全版重写
from typing import Literalfrom pydantic import BaseModelfrom jinja2.sandbox import SandboxedEnvironment
TEMPLATES = { "weekly": "Hello {{ name }}, here is your weekly summary: {{ summary }}",}
class ReportIntent(BaseModel): template_id: Literal["weekly"] summary: str
sandboxed_env = SandboxedEnvironment()
def render_report(intent: ReportIntent, name: str) -> str: template = TEMPLATES[intent.template_id] return sandboxed_env.from_string(template).render( name=name, summary=intent.summary, )LLM 可以影响 summary 变量值,但不能影响模板源码;模板源码来自开发者维护的 TEMPLATES 字典。SandboxedEnvironment 提供额外深度防御。
案例锚点
本站当前不在本页新增独立 ToLO-Template CVE 事实。动态 prompt/template 类报告如未核验,必须继续标为 pending。
子类之间会组合
真实系统经常一条功能触达多个 sink:
| 功能 | 可能的 ToLO 路径 |
|---|---|
| 数据分析 agent | ToLO-SQL 取数后,ToLO-Exec 生成可视化代码。 |
| 文件管理 agent | ToLO-Path 选择文件后,ToLO-Shell 调 CLI 处理。 |
| Web agent | ToLO-SSRF 抓页面后,页面内容作为 S_LLM^{rag} 或 tool result 影响后续 sink。 |
| 报告生成器 | ToLO-Template 渲染报告,报告中的路径或 URL 再进入 ToLO-Path / ToLO-SSRF。 |
| Agent 状态恢复 | ToLO-Deser 恢复出 dict,字段再被拼入 ToLO-SQL 或 ToLO-Shell。 |
分类时以”每条 source-to-sink 路径”为单位,而不是以”每个产品功能”为单位。同一个 issue 或 CVE 可以包含多条 ToLO 路径——例如 Vanna AI 的报告同时涉及 SQL 路径与 Plotly 代码执行路径。
如何使用七子类
分析一个新案例时,不要先问”这是哪个 CVE”,而要先写四列:
| 列 | 要写什么 |
|---|---|
| Source | 哪个 S_LLM^{...} 子集产生或承载污染 |
| Transform | parser、message wrapper、agent action、tool input 如何传播 |
| Sink | 进入七子类中的哪一种危险操作 |
| Guard | 是否存在类型匹配的 C_SAFE^{...} |
判定流程:
- 如果 source 不清楚 → 不能判 ToLO。
- 如果 sink 不敏感 → 可能只是输出展示问题,不是 ToLO。
- 如果 guard 类型匹配且强制 → 可能是已缓解路径(继续审查可绕过性)。
- 如果 guard 只是 prompt 约束、裸字符串类型检查或日志记录 → 通常不能切断 ToLO。
快速归类表
如果 S_LLM 进入 | 先按哪类看 | 优先找哪类 guard |
|---|---|---|
pickle.loads / unsafe yaml.load / object loader | ToLO-Deser | C_SAFE^{safe-codec} + type allowlist |
eval / exec / REPL / code runner | ToLO-Exec | language subset + C_SAFE^{capability} |
os.system / shell=True / Bash tool | ToLO-Shell | C_SAFE^{parameterized} + command allowlist |
| raw SQL / Cypher / query runner | ToLO-SQL | template + params + DB capability |
open / Path.read_text / file loader | ToLO-Path | resolved root allowlist |
requests.get / httpx / browser fetch | ToLO-SSRF | scheme/host/IP allowlist + egress capability |
Template(...) / from_string(...) | ToLO-Template | fixed template + sandbox |
与传统同名 CWE 的关键区别
七个子类的 sink 端都能映射到既有 CWE,这正是 ToLO 与现有安全体系兼容的地方。但 ToLO 的 source 端不同:
| 维度 | 传统 CWE | ToLO |
|---|---|---|
| Source | HTTP 参数、cookie、上传文件、CLI 参数等显式外部输入。 | LLM 输出、tool call、RAG content、structured output 等被视为框架内部数据的输入。 |
| 开发者意图 | 常常是 misuse(忘了过滤或拼接错误)。 | 常常是 architectural choice(有意让模型决定代码、命令、SQL、路径、URL 或模板)。 |
| 观测信号 | WAF、access log、表单字段比较直接。 | 模型中间输出、agent state 和 tool argument 常缺少审计。 |
| 修复方式 | escape、参数化、路径规范化等较标准。 | 需要先收窄模型可表达的动作空间,再做类型匹配的 sink 前防御。 |
具体每子类的差异:
ToLO-Deservs CWE-502:传统 source 是 HTTP body,开发者通常会避免反序列化它们;ToLO 的 source 是”框架内部对象”,反序列化路径常根本不被识别为敏感操作。ToLO-Execvs CWE-94:ToLO-Exec的 source 是有意设计为接受 LLM 代码的输入。修复必须重新设计沙箱与 capability 边界。ToLO-SQLvs CWE-89:传统修复是 parameterized query;text-to-SQL 整条 SQL 由 LLM 生成,参数化无从下手,只能靠 schema 限制 + RBAC。ToLO-Shellvs CWE-78:agent 框架常有意让 LLM 指定完整命令,shlex.quote与设计意图冲突。ToLO-Path/ToLO-SSRF/ToLO-Templatevs 同名 CWE:经典防御假设 source 是用户输入,但 ToLO 的 source 是被开发者视为”系统内部决定”的 LLM 输出,监控与限制信号都更弱。
因此 ToLO 不要求新增 CWE 才能成立。它是一个以 S_LLM 为锚点的信任边界问题,在 sink 端复用已有 CWE family。
分类不闭合性声明
七个 ToLO-{Deser,Exec,Shell,SQL,Path,SSRF,Template} 是本站当前的教育性锚点,不是穷尽枚举。ToLO family 由 source class S_LLM 锚定;如果未来公开案例显示 LLM 输出稳定进入新的危险 sink family(例如 OS 资源限制操作、序列化协议字段、RPC 调用、IPC 消息), taxonomy 可以扩展。
新增子类应同时满足两个条件:
| 条件 | 说明 |
|---|---|
| 新的被保护对象或后果 | 不能只是现有 sink 的别名。 |
| 新的类型匹配 sanitizer 需求 | 如果修复方式完全落在现有七类,优先作为现有子类的变体。 |
否则优先作为某个现有子类的变体记录。
读完检查
判断下面说法是否正确:
| 说法 | 判断 |
|---|---|
| LLM 输出进入页面展示,一定是 ToLO。 | 错。要看是否进入危险 sink。展示到 HTML 时如果用 textContent 输出,不构成 ToLO;如果通过 innerHTML 渲染,可能是 XSS,但 source 端通常归 OWASP LLM05 而非 ToLO。 |
LLM 输出经过 json.loads 后就安全。 | 错。JSON 字段仍可能进入 SQL、shell、path 等 sink。 |
Pydantic(field: str) 是 C_SAFE^{schema}。 | 通常不是。只有 Literal、Enum、严格 pattern/范围约束等才可能算。 |
subprocess.run(["bash", "-c", llm_cmd], shell=False) 能防 ToLO-Shell。 | 不能。bash -c 仍解释 LLM 字符串。 |
subprocess.run(['git', user_branch], shell=False) 一定安全。 | 未必。如果 user_branch 来自模型,仍可能 inject 命令选项(如 --upload-pack=...)。 |
| taxonomy 未来不能扩展。 | 错。七类是教育性锚点,不是闭合集合。 |
下一步阅读
- Defensive Patterns:五类
C_SAFE的工程含义与组合。 - Trust Boundaries:把这些子类放进 attacker、source、sink、guard 的威胁模型里。
- Sources and Sinks:source 集合的 5 子集与 sanitizer 集合的 5 类如何落到静态分析谓词。