Skip to content

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 映射到不同后果:反序列化、代码执行、命令执行、数据库查询、文件访问、网络访问与模板执行。

怎么读这一页

每个子类按统一模板:

  1. 是什么:对应哪类 sink、典型函数、所属 CWE。
  2. 为什么对 ToLO 重要:工程动机和 LLM 场景下的特殊性。
  3. 典型 source shapes:五个 S_LLM 子集如何承载这条路径。
  4. 典型 sink shapes:同一类 sink 在不同 API 下的形态。
  5. 最小教学骨架:不可直接复现的最简化骨架,用来理解形态。
  6. 什么不是 sanitizer:常见错配,看起来像防御但其实不切断危险语义。
  7. 有效 C_SAFE patterns:用哪类 sanitizer,如何组合。
  8. 安全版重写(部分子类配):一段教学性的”如果按 ToLO 思路修,大概是这样”代码。
  9. 案例锚点:已有公开 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.loadsyaml.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-ExecS_LLM 数据作为程序代码进入 evalexeccompile、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 OpenAI
client = 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

四列拆解:

SourceTransformSinkGuard
S_LLM^{direct} (message.content)(无,直接传给 eval)eval(...) (CWE-94)缺失

什么不是 sanitizer

看起来像防御为什么不算
黑名单 importopenos 等关键字Python/JS 语法空间太大,黑名单不可完整。
长度限制 len(code) < 100100 字符够写 __import__('os').system(...)
Pydantic(code: str)只验证类型,不限制语义。
prompt 中写”只输出安全代码”属于通道侧约束,不是 sink 前 sanitizer。
旧版或未审计 Python sandboxsandbox 本身必须被当作高风险组件,不能只凭名称认定为 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-29374LangChain LLMMathChainPythonREPLpending
CVE-2023-36258LangChain PALChain(PAL)→ 代码执行pending
CVE-2023-39631LangChain _evaluate_expressionnumexpr 历史变体pending

这些锚点在本站保持 pending,除非后续用 NVD、MITRE、GHSA 或项目官方 advisory 核验。

§2 ToLO-Shell —— LLM 输出当 shell 命令

是什么

ToLO-ShellS_LLM 数据进入操作系统命令执行 sink:subprocess.run(..., shell=True)os.systemos.popencommands.getoutputpexpect.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 中的 cmdcommandargs
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 tool
import subprocess
@tool
def run_command(cmd: str) -> str:
"""执行一条 shell 命令"""
return subprocess.run(cmd, shell=True, capture_output=True).stdout.decode()
# ^^^^^^^^^^ ← ToLO-Shell sink,且 shell=True 让攻击面最大

四列拆解:

SourceTransformSinkGuard
S_LLM^{framework} (tool_call.arguments)LangChain 自动反序列化 argssubprocess.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 Literal
from pydantic import BaseModel
import 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()

四个变化点:

  1. Literal 让模型只能”选命令”,不能”写命令”。
  2. 网络段在应用层 allowlist。
  3. subprocess.run 用 list 形式,shell=False 是默认。
  4. timeout,避免慢命令拖死会话。

案例锚点

参见 Public Case Studies 中的公开列表。本站当前不在本页新增具体 ToLO-Shell CVE 事实;LangChain _run 的 ShellTool 类家族历史上多次被报告,具体 CVE 待核验。

§3 ToLO-SQL —— LLM 输出当数据库查询

是什么

ToLO-SQLS_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}tablecolumnwhereorder_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

四列拆解:

SourceTransformSinkGuard
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”的 promptprompt 不是强制策略。
普通 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 Literal
from 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-5826Vanna AI text-to-SQL 路径中的 Plotly 代码执行(主路径属 ToLO-Exec)pending
CVE-2024-8309LangChain GraphCypherQAChain(Cypher 执行,广义 query-language sink)pending

Vanna AI 的案例本站把公开来源支持的主路径写成 ToLO-Exec via Plotly code;text-to-SQL 只作为相关链路背景,不放在本表作为 ToLO-SQL 主锚点。

§4 ToLO-Path —— LLM 输出当文件路径

是什么

ToLO-PathS_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 中的 pathfilenamedirectory
S_LLM^{structured}{"doc_id": "...", "target_path": "..."} 的路径字段。
S_LLM^{parsed}从模型文本提取出的文件名。
S_LLM^{rag}RAG metadata sourcepathuri 被用来读原文。
S_LLM^{direct}模型直接建议要打开或保存的文件路径。

典型 sink shapes

Sink shape风险语义
open(path)Path(path).read_text()write_text()读写任意路径。
shutil.copy/move/rmtreeos.remove移动、覆盖、删除。
os.makedirs、archive extract创建目录或解压到越权位置。
framework file loader看似文档加载,实际接受路径并访问文件系统。

最小教学骨架

@tool
def read_note(name: str) -> str:
"""读取一份笔记"""
return open(f"/srv/notes/{name}").read() # ← ToLO-Path (name = "../../etc/passwd")

四列拆解:

SourceTransformSinkGuard
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
使用 pathlibPath("/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 Path
from typing import Literal
from 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 限定枚举
@tool
def 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()

三个关键点:

  1. Literal 把”路径”收窄成”枚举的 doc_id”。
  2. 应用层 ALLOWED_DOCS 字典做 ID → 文件名映射,LLM 不直接给文件名。
  3. resolve() 之后用 is_relative_to(ROOT) 兜底——这一步即使被插入符号链接也能拒绝

案例锚点

本站当前不在本页新增独立 ToLO-Path CVE 事实。LangChain FileTool 类家族历史上有 path 类报告;具体 CVE 待核验,必须继续标为 pending

§5 ToLO-SSRF —— LLM 输出当 URL 或网络目标

是什么

ToLO-SSRFS_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=...
  • 内网管理 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 中的 urlendpoint
S_LLM^{structured}structured output 的 schemehostpathquery 字段。
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 callerLLM 控制 base URL、path、headers 或 method。

最小教学骨架

@tool
def fetch_url(url: str) -> str:
"""抓取 URL 内容"""
return requests.get(url, timeout=5).text # ← ToLO-SSRF

四列拆解:

SourceTransformSinkGuard
S_LLM^{framework} (tool_call.arguments["url"])(无)requests.get(url) (CWE-918)缺失

什么不是 sanitizer

看起来像防御为什么不算
只允许 http / httpsscheme 正确不代表 host 安全。
字符串黑名单 if "169.254" in url: raiseIP 有多种表示法(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, socket
from 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-DeserS_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: strobject_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 loaderYAML 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

四列拆解:

SourceTransformSinkGuard
S_LLM^{direct} (base64 编码后嵌在响应里)base64.b64decodepickle.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 allowlistPython pickle 反序列化的 gadget chain 历史上多次绕过 restrictions。

有效 C_SAFE patterns

Pattern用法
C_SAFE^{safe-codec}json.loadsyaml.safe_loadast.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-TemplateS_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_bodysubject_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

四列拆解:

SourceTransformSinkGuard
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 Literal
from pydantic import BaseModel
from 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 路径
数据分析 agentToLO-SQL 取数后,ToLO-Exec 生成可视化代码。
文件管理 agentToLO-Path 选择文件后,ToLO-Shell 调 CLI 处理。
Web agentToLO-SSRF 抓页面后,页面内容作为 S_LLM^{rag} 或 tool result 影响后续 sink。
报告生成器ToLO-Template 渲染报告,报告中的路径或 URL 再进入 ToLO-Path / ToLO-SSRF
Agent 状态恢复ToLO-Deser 恢复出 dict,字段再被拼入 ToLO-SQLToLO-Shell

分类时以”每条 source-to-sink 路径”为单位,而不是以”每个产品功能”为单位。同一个 issue 或 CVE 可以包含多条 ToLO 路径——例如 Vanna AI 的报告同时涉及 SQL 路径与 Plotly 代码执行路径。

如何使用七子类

分析一个新案例时,不要先问”这是哪个 CVE”,而要先写四列:

要写什么
Source哪个 S_LLM^{...} 子集产生或承载污染
Transformparser、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 loaderToLO-DeserC_SAFE^{safe-codec} + type allowlist
eval / exec / REPL / code runnerToLO-Execlanguage subset + C_SAFE^{capability}
os.system / shell=True / Bash toolToLO-ShellC_SAFE^{parameterized} + command allowlist
raw SQL / Cypher / query runnerToLO-SQLtemplate + params + DB capability
open / Path.read_text / file loaderToLO-Pathresolved root allowlist
requests.get / httpx / browser fetchToLO-SSRFscheme/host/IP allowlist + egress capability
Template(...) / from_string(...)ToLO-Templatefixed template + sandbox

与传统同名 CWE 的关键区别

七个子类的 sink 端都能映射到既有 CWE,这正是 ToLO 与现有安全体系兼容的地方。但 ToLO 的 source 端不同:

维度传统 CWEToLO
SourceHTTP 参数、cookie、上传文件、CLI 参数等显式外部输入。LLM 输出、tool call、RAG content、structured output 等被视为框架内部数据的输入。
开发者意图常常是 misuse(忘了过滤或拼接错误)。常常是 architectural choice(有意让模型决定代码、命令、SQL、路径、URL 或模板)。
观测信号WAF、access log、表单字段比较直接。模型中间输出、agent state 和 tool argument 常缺少审计。
修复方式escape、参数化、路径规范化等较标准。需要先收窄模型可表达的动作空间,再做类型匹配的 sink 前防御。

具体每子类的差异:

  • ToLO-Deser vs CWE-502:传统 source 是 HTTP body,开发者通常会避免反序列化它们;ToLO 的 source 是”框架内部对象”,反序列化路径常根本不被识别为敏感操作。
  • ToLO-Exec vs CWE-94:ToLO-Exec 的 source 是有意设计为接受 LLM 代码的输入。修复必须重新设计沙箱与 capability 边界。
  • ToLO-SQL vs CWE-89:传统修复是 parameterized query;text-to-SQL 整条 SQL 由 LLM 生成,参数化无从下手,只能靠 schema 限制 + RBAC。
  • ToLO-Shell vs CWE-78:agent 框架常有意让 LLM 指定完整命令,shlex.quote 与设计意图冲突。
  • ToLO-Path / ToLO-SSRF / ToLO-Template vs 同名 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}通常不是。只有 LiteralEnum、严格 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 类如何落到静态分析谓词。