Core ToLO Patterns
ToLO 不是单一漏洞,而是同一信任模型失效与不同下游 sink 结合后分化出的具体形态。本页按 sink 类型列出七子类,每个对应一个已有 CWE,但 source 端语义是新的。
七子类的命名是教学和检测上的便利。它们不表示七个互相独立的漏洞,而是帮助读者把同一条 source family 映射到不同后果:代码执行、命令执行、数据库查询、文件访问、网络访问、模板执行和反序列化。
怎么读这一页
每个子类按统一模板:
- 是什么:对应哪类 sink,典型函数。
- 最小教学代码:不可直接复现的最简化骨架,用来理解形态。
- 典型 source-transform-sink-guard 四列拆解。
- 真实公开案例(标注 verification 状态)。
- 为什么开发者会写出这种代码:工程动机。
- 类型匹配的 sanitizer:用哪类
C_SAFE。 - 常见错配:看起来像 sanitizer 但其实不算。
所有代码片段都是教学骨架,不含 exploit payload 和 PoC 复现指令。
先修概念:什么是 sink
Sink 是”数据到这里会产生安全后果”的位置。
例如:把字符串展示在聊天框里通常不是 sink;把字符串交给 eval、数据库、shell 或文件系统就是 sink。
ToLO 的七子类都按 sink 命名,因为后果由 sink 决定。同样是 LLM 输出:
- 进入
eval→ 后果是代码执行 - 进入
open→ 后果是文件访问 - 进入
requests.get→ 后果是网络访问
如果你对 sink 的概念不熟,回去读 先修知识 §5。
§1 ToLO-Exec — LLM 输出当代码执行
是什么
LLM 输出作为代码字符串进入 eval / exec / compile / PythonREPL / IPython kernel / Jupyter 等代码执行 sink。对应 CWE-94(Code Injection)。
常见于:PAL(Program-Aided Language model)、code-interpreter、symbolic-math、Plotly 自动可视化、数据分析助手。
最小教学代码
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) | 缺失 |
真实公开案例
| CVE | 项目 / 组件 | verification |
|---|---|---|
| CVE-2023-29374 | LangChain LLMMathChain → PythonREPL | pending |
| CVE-2023-36258 | LangChain PALChain (PAL) → 代码执行 | pending |
| CVE-2023-39631 | LangChain _evaluate_expression → numexpr 历史变体 | pending |
为什么开发者会写出这种代码
开发者通常本来就希望模型”写一段代码帮我算”,所以会把执行设计成正常功能。这不是 misuse,是 architectural choice。修复必须重新设计沙箱与 capability 边界,不能只靠 input validation。
类型匹配的 sanitizer
- 首选
C_SAFE^safe-codec:把”任意 Python”收窄到”数学表达式子集”,用numexpr.evaluate(expr, global_dict={}, local_dict={})或ast.literal_eval。 - 必备
C_SAFE^capability:执行隔离(RestrictedPython风险高,生产用 Docker / gVisor / Firecracker)。 - 辅助
C_SAFE^schema:用 PydanticLiteral把”代码语言”限制(如lang: Literal["math"])。
常见错配
- ❌ 黑名单关键字(
if "import" in expr: raise)— 一定可绕。 - ❌ 长度限制(
if len(expr) > 100: raise)— 100 字符够写__import__('os').system(...)。 - ❌
Pydantic(expr: str)— 字符串可装任意 payload。 - ❌ “我们用了 RestrictedPython 旧版” — Python sandbox 史上多次被绕。要么用真容器隔离,要么用真正受限的求值器(numexpr / ast.literal_eval)。
§2 ToLO-Shell — LLM 输出当 shell 命令
是什么
LLM 输出进入 subprocess.run(..., shell=True) / os.system / os.popen / commands.getoutput / pexpect.run(shell)。对应 CWE-78(OS Command Injection)。
常见于:agent 应用的 shell tool、SRE diagnostic agent、CI/CD 助手、自动化运维。
最小教学代码
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) | 缺失 |
真实公开案例
参见 Public Case Studies 公开 CVE 列表;LangChain _run 的 ShellTool 类家族历史上多次被报告。具体 CVE 待核验。
为什么开发者会写出这种代码
Agent framework 常常有意让 LLM 指定完整命令。这种”灵活性”和 shlex.quote 这种保守做法直接冲突。开发者认为”反正是 LLM 选的命令,模型不会乱来” —— 这是 ToLO 的核心错误信任。
类型匹配的 sanitizer
- 首选
C_SAFE^parameterized:永远不要shell=True,用 list 形式subprocess.run([executable, arg1, arg2])。 - 必备
C_SAFE^allowlist:executable 必须是枚举白名单内的几个固定 binary;参数也限制取值。 - 辅助
C_SAFE^schema:用 PydanticLiteral["ping", "traceroute"]让模型只能”选命令”,不能”写命令”。 - defense in depth
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()常见错配
- ❌
shlex.quote(cmd)给整条 LLM 输出转义,然后shell=True— quote 只防引号,不防整条命令被替换。 - ❌ 黑名单字符(
if ";" in cmd or "|" in cmd: raise)— 可绕(换行符、${IFS}、 backtick 等)。 - ❌ “命令必须以
ping开头” —ping x; rm -rf /仍以ping开头。
§3 ToLO-SQL — LLM 输出当 SQL 执行
是什么
LLM 生成的 SQL 直接送入 cursor.execute / connection.execute / pandas.read_sql / SQLAlchemy text() / Cypher / GraphQL。对应 CWE-89(SQL Injection)。
常见于:text-to-SQL、自然语言 BI 工具、自动 ETL、数据分析 agent。
最小教学代码
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) | 缺失 |
真实公开案例
| CVE | 项目 / 组件 | verification |
|---|---|---|
| CVE-2024-5826 | Vanna AI text-to-SQL → 直接执行 | pending |
| CVE-2024-8309 | LangChain GraphCypherQAChain (Cypher 执行) | pending |
为什么开发者会写出这种代码
Text-to-SQL 的核心需求就是让 LLM 自由生成 SQL。传统参数化(execute("... WHERE id = %s", (uid,)))只在 SQL 结构固定时有效。整条 SQL 由 LLM 决定时,参数化无从下手。
类型匹配的 sanitizer
- 首选
C_SAFE^capability:用只读数据库账号(DROP/UPDATE/DELETE 直接被数据库拒绝)。 - 必备
C_SAFE^allowlist:把可访问的表/列在应用层 allowlist;模型给的”表名”先经过 dict 映射。 - 架构改动:把 LLM 输出从”整条 SQL”收窄到”WHERE 条件值”,或”枚举查询模板 + 参数”。
- defense in depth:Query rewriter(在执行前用 SQL parser 重写 LLM 输出,确认只是 SELECT,确认表/列在白名单)。
一段安全版重写
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 部分常见错配
- ❌
cursor.execute(llm_sql)即使有read_only=Trueconnection 配置 — 还是建议用专门只读账号。 - ❌ 黑名单关键字 — SQL 语法有几百种写法可绕(注释、union、子查询、INFORMATION_SCHEMA)。
- ❌ “用了 SQLAlchemy ORM” — ORM 自动参数化只对
Model.filter(col=value)形式生效。session.execute(text(llm_sql))仍然完全暴露。
§4 ToLO-Path — LLM 输出当文件路径
是什么
LLM 输出作为文件路径进入 open / pathlib.Path.read_text|write_text / shutil / os.makedirs / tarfile.extract / zipfile.extract。对应 CWE-22(Path Traversal)。
常见于:文件管理 agent、笔记 / 知识库写入、上传整理工具、log 查看器。
最小教学代码
@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) |
真实公开案例
未在本站当前 CVE 列表中独立锚定;LangChain FileTool 类家族历史上有 path 类报告,具体 CVE 待核验。
为什么开发者会写出这种代码
文件管理 agent 天然需要文件路径。开发者直觉是”模型只会给合理的 name”,不会想到 ../、绝对路径、符号链接。
类型匹配的 sanitizer
- 首选
C_SAFE^allowlist:p = (ROOT / user_path).resolve(); if not p.is_relative_to(ROOT): raise。先 resolve 再判断,避免符号链接 /..绕过。 - 必备
C_SAFE^schema:文件 ID 映射 — 让 LLM 只能给doc_id(枚举),应用层映射到真实路径。 - defense in depth
C_SAFE^capability:进程级 chroot / 容器 mount 隔离 / seccomp 限制文件 syscall。
一段安全版重写
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()常见错配
- ❌ 正则过滤
..(if ".." in name: raise)— Windows 上..\\..\\不被命中;符号链接也不命中。 - ❌
os.path.join(ROOT, name)不做 resolve 检查 —name="/etc/passwd"时直接返回/etc/passwd(os.path.join遇绝对路径会丢弃前面)。 - ❌ “我们用了
pathlib所以安全” —Path("/srv/notes") / "../../etc/passwd"等价于Path("/srv/notes/../../etc/passwd"),必须 resolve。
§5 ToLO-SSRF — LLM 输出当 URL
是什么
LLM 输出作为 URL 进入 requests.get / requests.post / httpx / urllib.urlopen / aiohttp / urllib3。对应 CWE-918(SSRF)。
常见于:web-fetch 工具、URL preview、API caller、外部数据集成 agent、爬虫助手。
最小教学代码
@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) | 缺失 |
为什么 SSRF 在 LLM agent 里特别危险
SSRF 的传统危害:让服务器访问内网。在 LLM agent 里还多了一层:服务器拉回的内容会被作为 ToolMessage 写回 LLM 上下文,污染下一轮决策(C2 通道)。这是一种”自我放大”。
被 SSRF 拉到的常见目标:
- 云元数据 IMDS:
http://169.254.169.254/latest/meta-data/iam/security-credentials/(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) - file:// scheme:在
urlopen接受 file scheme 时,可以读本地文件
类型匹配的 sanitizer
- 首选
C_SAFE^allowlist:host allowlist + scheme 限制(http/https)+ 端口限制。 - 必备:内网 IP / loopback / 链路本地地址 block(包括 IPv6)。
- 架构 layer:把 outbound HTTP 限制在专门的 egress proxy,proxy 层做最终允许列表。
- DNS rebinding 防御:解析 IP 后再次校验(防御 attacker 改 DNS 在第二次解析时返回内网 IP)。
一段安全版重写
import requests, ipaddress, socketfrom urllib.parse import urlparsefrom typing import Literal
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常见错配
- ❌ “只允许
http/https” 不够 — host 仍可指向内网。 - ❌ 字符串黑名单
if "169.254" in url: raise—0x0a.0x00.0x00.0x01/2130706433(十进制 IP)绕过。 - ❌ 一次性 host 检查 — DNS rebinding 可在第二次解析时返回不同 IP。
§6 ToLO-Deser — LLM 输出当反序列化输入
是什么
LLM 输出或 LLM-influenceable 数据进入 pickle.loads / dill.loads / joblib.load / torch.load (default) / yaml.load / marshal.loads。对应 CWE-502(Deserialization of Untrusted Data)。
常见于:cache / 持久化中间结果、agent state 读写、自定义模型加载、langchain loads / dumps 历史路径。
最小教学代码
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) | 缺失 |
真实公开案例
LangChain langchain.load.loads / serializable 体系历史上有 deserialization 安全考虑,具体 CVE 待核验。本站案例列表中暂未独立条目。
为什么开发者会写出这种代码
性能或便利:pickle 序列化任何 Python 对象,yaml 比 JSON “类型更丰富”。torch.load 默认行为是反序列化 pickle。开发者认为”这是我们自己存的数据,不会被改”,忘了 LLM 输出可能影响”我们存的数据”。
类型匹配的 sanitizer
- 首选
C_SAFE^safe-codec:用json.loads;yaml用yaml.safe_load;模型权重用torch.load(..., weights_only=True)或 safetensors。 - 架构改动:不要让 LLM 输出参与 pickle 流。如果必须持久化 LLM 状态,只持久化字符串字段(经 schema 验证)。
- defense in depth:即使解码用 safe codec,解码后的字段仍需做内容校验。
常见错配
- ❌ 加密包装(
pickle.loads(decrypt(blob)))— 攻击者只要能影响 plaintext 就可控。 - ❌ HMAC 签名 — 只防”第三方”篡改;如果 LLM 输出是签名的输入,攻击者通过 prompt injection 让模型生成”签名前内容”仍然有效。
- ❌ “用了
restricted_globals” — Python pickle 反序列化的 gadget chain 历史上多次绕过 restrictions。
§7 ToLO-Template — LLM 输出当模板字符串
是什么
LLM 输出作为模板字符串(而非模板变量)进入 jinja2.Template(...) / Environment.from_string(...) / Django template 直接渲染。对应 CWE-1336(Improper Neutralization of Special Elements Used in a Template Engine)。
⚠ 注意:这里的 source 是模板字符串本身,不是模板变量。模板变量永远应当被当 untrusted —— 那是 sink-side 的 autoescape 问题,不是 ToLO。
最小教学代码
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) | 缺失 |
为什么开发者会写出这种代码
让 LLM “生成 HTML / Markdown / 邮件模板” 等任务里,开发者会把整段输出当模板渲染。Jinja2 默认环境不沙盒化,Python 内省可触达任意对象 → 历史上多次 SSTI(Server-Side Template Injection)RCE。
类型匹配的 sanitizer
- 首选:模板字符串绝不来自 LLM。模板由开发者固定,LLM 只填变量。
- 如果必须 dynamic template,用
jinja2.sandbox.SandboxedEnvironment,且配合白名单 filter / function。 C_SAFE^capability:配合执行隔离(容器,无网络,只读 FS)。
常见错配
- ❌ 用
Environment(autoescape=True)— autoescape 只 HTML escape 变量输出,不防 attacker 控制模板字符串本身。 - ❌ “我们替换了
{{字符” — Jinja 支持{%- if -%}等 100 种语法形式。
子类之间会组合
真实框架里,一个功能可能同时触达多个 sink。例如:
- text-to-SQL 应用先让 LLM 生成 SQL(
ToLO-SQL),再让 LLM 生成 Plotly 代码做可视化(ToLO-Exec)。 - 文件管理 agent 先让 LLM 选择路径(
ToLO-Path),再调用 shell 命令处理文件(ToLO-Shell)。 - Web agent 先 SSRF 抓内容(
ToLO-SSRF),再把内容做模板渲染(ToLO-Template)。
因此分类时以”每条 source-to-sink 路径”为单位,而不是以”每个产品功能”为单位。同一个 CVE 或同一个 issue 可能包含多条 ToLO 路径。
如何使用七子类
分析一个新案例时,不要先问”这是哪个 CVE”,而要先写四列:
| 列 | 要写什么 |
|---|---|
| Source | 哪个 S_LLM 子集产生或承载污染 |
| Transform | parser、message wrapper、agent action、tool input 如何传播 |
| Sink | 进入七子类中的哪一种危险操作 |
| Guard | 是否存在类型匹配的 C_SAFE |
如果 source 不清楚 → 不能判 ToLO。 如果 sink 不敏感 → 可能只是输出展示问题。 如果 guard 类型匹配 → 可能是已缓解路径。 如果 guard 只是 prompt 约束或裸字符串类型检查 → 通常不能切断 ToLO。
快速归类表
| 如果 LLM 输出进入 | 先按哪类看 | 优先检查什么 |
|---|---|---|
pickle.loads / yaml.load | ToLO-Deser | 是否换成 safe codec |
eval / exec / code runner | ToLO-Exec | 是否有 sandbox 和 capability |
os.system / shell=True | ToLO-Shell | 是否避免 shell 和命令白名单 |
cursor.execute / raw query | ToLO-SQL | 是否参数化、只读、限制表/列 |
open / Path.read_text | ToLO-Path | 是否规范化并限制根目录 |
requests.get(url) | ToLO-SSRF | 是否限制 host、scheme、内网 |
Template(...).render | ToLO-Template | 模板是否固定、是否 sandbox |
与传统同名 CWE 的关键区别
每一子类的 sink 端与传统 CWE 一致,但 source 端是新的。差异主要在三点:
- 开发者意图:传统 CWE 通常是 misuse(开发者忘了过滤);ToLO 是 architectural choice(开发者有意让 LLM 决定 sink 参数)。
- 可观测性:传统 source 有 nginx log / WAF;LLM 输出几乎无审计。
- 修复路径:传统 CWE 修复路径标准(参数化 / escape);ToLO 修复路径更架构化(收窄动作空间、capability gate)。
具体每子类:
- 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与设计意图冲突。
剩余三类(Path / SSRF / Template)的区别原理类似:经典防御假设 source 是用户输入,但 ToLO 的 source 是被开发者视为”系统内部决定”的 LLM 输出,监控与限制信号都更弱。
分类不闭合性声明
以上七类是基于已观察到 CVE 的归纳,不是穷尽枚举。如果在后续公开案例中发现新的 sink 子类(例如 LLM 输出进入 OS 资源限制操作、序列化协议字段、RPC 调用、IPC 消息等),允许扩展。ToLO 这一 family 由 source class S_LLM 锚定,sink 子类是 open taxonomy。
扩展 taxonomy 时需要谨慎。新增子类应满足两个条件:
- sink 会影响明确的被保护对象。
- 该模式不能被现有七类自然覆盖。
否则优先作为某个现有子类的变体记录。
读完检查
判断下面说法是否正确:
- “LLM 输出进入页面展示,一定是 ToLO。”
- 错。要看是否进入危险 sink。展示到 HTML 时如果浏览器 textContent 输出,不构成 ToLO;如果通过
innerHTML渲染,可能构成 XSS 风险,但 source 端通常归 OWASP LLM05 而非 ToLO。
- 错。要看是否进入危险 sink。展示到 HTML 时如果浏览器 textContent 输出,不构成 ToLO;如果通过
- “LLM 输出进入
json.loads后就安全。”- 错。解析后的字段仍可能进入 SQL、shell、path 等 sink。
- “LLM 输出进入
subprocess.run(['git', 'status'], shell=False)一定安全。”- 未必。还要看命令和参数是否被模型自由控制、当前会话是否有执行 capability。如果
['git', user_branch]中user_branch来自模型,仍可能 inject 命令选项。
- 未必。还要看命令和参数是否被模型自由控制、当前会话是否有执行 capability。如果
下一步阅读
- Defensive Patterns:五类
C_SAFE的工程含义与组合。 - Trust Boundaries:把这些子类放进 attacker、source、sink、guard 的威胁模型里。
- Sources and Sinks:source 集合的 5 子集与 sanitizer 集合的 5 类。