Sources and Sinks
ToLO 检测建立在标准污点分析框架上:三个谓词集合(source、sink、sanitizer)加传播规则。本节给出三个集合的具体划分,可直接映射到 CodeQL DataFlow::Configuration 或 TaintTracking::Configuration。
第一次读这些词时可以这样理解:
- source:污点起点,即 LLM 生成或可影响的数据进入程序的位置。
- sink:危险操作入口,即数据一旦进入就可能影响进程、文件系统、网络、数据库或模板执行等被保护对象的位置。
- sanitizer:能消除某一类 sink 风险的处理。
- barrier / guard:静态分析实现里的阻断点。它通常来自 sanitizer,但不是所有检查都能当 barrier。
- taint path:从 source 到 sink 的完整污点路径。
- dataflow:值在变量、字段、容器、函数参数和返回值之间传播的关系。
- interprocedural flow:跨函数或跨方法的数据流。
- false positive:误报,规则报告了但路径实际被有效防御切断。
- false negative:漏报,规则没有报告但未防御路径真实存在。
- type-matched sanitizer:与 sink 语义匹配的 sanitizer,例如 SQL 参数化只能切断 SQL 值位置风险,不能切断 path 或 shell 风险。
这一页的重点是把”LLM 输出不可信”变成可审查的集合定义。不要把 source 写成”所有字符串”,也不要把 sanitizer 写成”任何校验函数”。ToLO 的有效性来自边界清晰:
- source 必须能追溯到 LLM 或 LLM-influenceable 数据。
- sink 必须能影响被保护对象。
- sanitizer 必须与 sink 语义匹配。
这一页的结构
- 先修概念:污点传播
- Source 集合
S_LLM(五子集)+ 每子集的代码模式 - Source 建模优先级
- 子集互相转化
- 为什么 Pydantic 不构成 sanitizer
- Sink 集合
S_DANGER(七类)+ 复用 CodeQL 标准库 + 危险参数位置 - Sanitizer 集合
C_SAFE(五类)+ 类型匹配判定 + 矩阵 - 好 barrier 与坏 barrier(每个 sink 一段对照)
- 从集合到谓词
- 三个容易混淆的判断
- ToLO 谓词(集合视图)
- 告警解释模板
先修概念:污点传播
污点传播可以理解为给数据贴标签:
- 某个值来自不可信来源,就贴上
tainted标签。 - 只要它被赋值、拼接、放进对象、解析成字段,标签就跟着走。
- 如果它经过有效 sanitizer,标签才可能被移除。
最小例子:
content = ai_message.content # tainted (从 LLM 输出来)obj = json.loads(content) # obj 仍然 tainted (json.loads 不清洗)path = obj["path"] # path 仍然 tainted (dict 取字段)open(path) # tainted 值进入 sink → 报告json.loads 没有移除路径风险,所以污染继续传播。
下一节展示更多传播规则的细节:容器传播、字符串变换、框架分派 → 见 Predicate Rules。
Source 集合 S_LLM(五子集)
S_LLM 的含义不是”所有用户输入”,也不只是”模型接口返回的自然语言文本”。它覆盖两类数据:
- LLM-produced:模型直接生成的文本、工具调用参数、结构化输出字段。
- LLM-influenceable:攻击者可通过 prompt、RAG、tool response 或模型供应链影响,并被框架当作可信内部内容继续处理的数据。
静态分析中,source 谓词应尽量指向最靠近框架信任边界的位置。例如 response.choices[0].message.content 比后面任意 str 变量更适合作 source,因为报告能解释”这个值为什么属于 ToLO”。
S_LLM^{direct} —— 模型直接生成的文本
直接 LLM API 客户端的返回字段。
| 框架 | 字段路径 |
|---|---|
| OpenAI Python | response.choices[0].message.content |
| OpenAI Python | response.choices[0].message.tool_calls[i].function.arguments |
| OpenAI Python | response.choices[0].message.function_call.arguments |
| OpenAI Python | response.choices[0].delta.content(streaming) |
| OpenAI Python | response.output_text(新 Responses API) |
| Anthropic Python | response.content[i].text(text blocks) |
| Anthropic Python | response.content[i].input(tool use blocks) |
| Google GenAI | response.text / response.candidates[0].content.parts[0].text |
| Google GenAI | response.candidates[0].content.parts[i].function_call.args |
| Cohere | response.text |
| Mistral | response.choices[0].message.content |
| Ollama / OpenAI 兼容 | 同 OpenAI 字段 |
CodeQL 建模思路:把上述 SDK 的客户端方法返回值的 .content / .text / tool-call arguments 等属性访问标为 source。需要识别 SDK 类型(openai.OpenAI、anthropic.Anthropic、google.generativeai.GenerativeModel 等)。
不要这样建模:
isSource(n) := n 的类型是 str这会把配置文件、日志字符串、固定 SQL 片段全部污染,导致 false positive。ToLO 的 source 必须能说明它来自 LLM 或 LLM-influenceable 框架数据。
S_LLM^{framework} —— 编排框架包装后的对象字段
编排框架把模型输出包装成对象后的字段。
| 框架 | 字段路径 |
|---|---|
| LangChain | AIMessage.content |
| LangChain | AIMessage.tool_calls[i]["args"] |
| LangChain | AIMessage.additional_kwargs |
| LangChain | AgentAction.tool_input(dict or str) |
| LangChain | AgentFinish.return_values["output"] |
| LangChain | ChatGeneration.text |
| LlamaIndex | Response.response |
| LlamaIndex | ChatResponse.message.content |
| LlamaIndex | AgentChatResponse.response |
| Haystack | Answer.data / GeneratedAnswer.data |
| AutoGen | Agent 消息列表内 content |
| CrewAI | Task.output.raw / result |
| Semantic Kernel | FunctionResult.value |
| LiteLLM | 与 OpenAI 同接口 |
| MCP | tools/call 返回的 content[i].text |
CodeQL 建模思路:为每个框架的 message / generation / agent action / response 类建立 type model,然后把这些类的 content / output / args 属性访问标为 source。
框架层 source 的常见难点是字段名看起来很普通。content、value、output 在普通应用里不一定危险;只有当它们属于 LLM message、agent action、tool call 或 generation 对象时才应进入 S_LLM^{framework}。
S_LLM^{parsed} —— 从前两类经字符串解析得到的派生字段
显式的 propagation step,因为它们通常需要 dataflow 标记中间转换:
data = json.loads(ai_message.content) # ← 显式传播obj = re.findall(r"...", ai_message.content)yaml_obj = yaml.safe_load(response.text)parsed = MyOutputParser.parse(ai_message.content)CodeQL 建模思路:在 TaintTracking::Configuration 中加 isAdditionalTaintStep 规则,把 json.loads(x)、yaml.safe_load(x)、re.search(x, y).group(...)、自定义 parser 调用都建成 x → return value 的污点传播。
S_LLM^{parsed} 是 source 子集,但更准确地说它经常表现为传播规则:原始 source 已经存在,parser 的返回值继承污染。不要把 parser 返回值当成新鲜可信数据。
S_LLM^{structured} —— 结构化输出与 function call 的字段
Pydantic / dataclass 实例的所有字段:
| 框架 | 字段路径 |
|---|---|
| OpenAI structured output | client.beta.chat.completions.parse(response_format=X) → 实例字段 |
| Anthropic tool use | response.content[i].input(dict) |
LangChain with_structured_output | llm.with_structured_output(X).invoke(...) → 实例 |
| Instructor | client.chat.completions.create(response_model=X) → 实例 |
| Outlines | outlines.generate.json(model, X) → 实例 |
| Marvin | marvin.cast(text, target=X) → 实例 |
重点:实例的所有字段默认都是 tainted,即使 schema 限制了形状。详见 §“为什么 Pydantic 不构成 sanitizer”。
判断结构化输出时,先问两个问题:
- 这个对象是否由 LLM 输出解析而来?
- 当前字段是否被收窄到安全枚举、正则子语言或固定能力集合?
第一问决定它是不是 source。第二问只决定某个字段能不能在特定 sink 前作为 sanitizer 候选。不要把整个 model 一次性标成已清洗。
S_LLM^{rag} —— RAG 检索器返回的文档内容字段
| 框架 | 字段路径 |
|---|---|
| LangChain | Document.page_content |
| LangChain | Document.metadata(也是 attacker-influenceable) |
| LlamaIndex | NodeWithScore.node.text |
| LlamaIndex | NodeWithScore.node.metadata |
| Haystack | Document.content / Document.meta |
| 任意 vector store | vector_store.similarity_search(...) 的返回值 |
攻击者可通过 RAG 投毒通道(C3)控制,与 S_LLM^{direct} 等价。
S_LLM^{rag} 在命名上容易误解:检索文档不一定由 LLM 生成,但它会进入 LLM 编排链路并被应用信任。只要攻击者能影响检索内容,它在 ToLO 规则里就应作为 LLM-influenceable source 处理。
子集之间互相转化
五个子集可以互相转化:
S_LLM^{rag} 进入 prompt → 影响 S_LLM^{direct}S_LLM^{direct} 被框架包装 → 变成 S_LLM^{framework}S_LLM^{framework} 经 parser → 变成 S_LLM^{parsed}S_LLM^{direct} 经 function calling → 变成 S_LLM^{structured}静态分析规则需要把这些转换当作传播步骤,而不是把它们误认为清洗。
┌──────────────┐ │ S_LLM^direct │ └──────┬───────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────┐ ┌────────────────┐ │S_LLM^framework│ │S_LLM^parsed│ │S_LLM^structured│ └───────┬──────┘ └──────────┘ └────────────────┘ │ ▼ parser/dispatch (re-enter)Source 建模优先级
第一层应覆盖直接 SDK:OpenAI、Anthropic、Google、Cohere、Mistral、Ollama 等 client 的返回字段。这一层对象少、语义稳定,是跨框架 baseline。
第二层覆盖旗舰框架:LangChain Python、LangChain.js、LlamaIndex Python 等。它们的 message、generation、agent action、tool call 包装层复杂,但生态影响大。
第三层覆盖 long-tail 框架:Haystack、AutoGen、CrewAI、Semantic Kernel、LiteLLM、Guidance、DSPy、Marvin 等。可以先做浅层 source 标记,再通过公开案例和人工 review 加深。
五个 source 子集怎么记
| 子集 | 白话解释 | 例子 |
|---|---|---|
direct | 直接 SDK 返回 | OpenAI / Anthropic response 字段 |
framework | 框架包装对象 | AIMessage.content、AgentAction.tool_input |
parsed | 从输出解析出来 | json.loads(ai.content)["path"] |
structured | 结构化输出字段 | Pydantic model 的字段 |
rag | 检索返回内容 | Document.page_content、metadata |
不要被符号吓到。它们只是说明:LLM 相关的不可信数据不只在一个对象里,而是在多个框架层之间流动。
为什么 Pydantic 不构成 sanitizer
这一点经常被误解,单独说明。
Pydantic 只验证类型与结构(str 是 str、字段存在、长度在范围内),它不验证内容语义。一个被验证为合法 str 的字段,其字符串内容仍可包含任意攻击 payload。
判定规则
只有当字段使用 Literal[...] / Enum / Annotated[str, StringConstraints(pattern=...)] 等约束时,才视为 sanitizer 候选;单纯的 str 字段不构成 sanitizer。
这一判定偏向 recall —— 可能产生 false positive(应用做了自实现校验但我们没识别),但 false negative 的代价更高。
最小例子
class Action(BaseModel): path: str这个 model 只说明 path 是字符串。"../../.env" 也是字符串,所以它不会阻止路径风险。
更强一点:
class Action(BaseModel): action: Literal["summarize", "search"]这里 action 被限制在两个值内,才有可能成为 schema sanitizer 的一部分。
CodeQL 表达方式
predicate isSchemaConstrainedField(Field f) { // Literal[...] 注解 f.getAnnotation().hasName("Literal") or // Enum 类型 f.getType().getABaseType*().hasQualifiedName("enum", "Enum") or // Annotated[str, StringConstraints(pattern=...)] exists(Annotation a | a = f.getAnnotation() and a.hasName("Annotated") and a.hasArgument(_, "StringConstraints"))}Sink 集合 S_DANGER(七类)
对应 Core ToLO Patterns 的七子类。这一集合不需要 LLM-specific 扩展 —— 经典 sink family 可复用,本站当前七类覆盖教学主线,但不声明穷尽。
直接复用 CodeQL 标准库:
| ToLO 子类 | CodeQL 复用 query/library |
|---|---|
ToLO-Deser | python/unsafe-deserialization |
ToLO-Exec | python/code-injection |
ToLO-Shell | python/command-line-injection |
ToLO-SQL | python/sql-injection |
ToLO-Path | python/path-injection |
ToLO-SSRF | python/server-side-request-forgery |
ToLO-Template | python/template-injection(部分需自建) |
JS / TS 等其他语言的 sink 集合复用同样的 CodeQL 标准库。
设计原则:Sink 复用
Sink 端不发明,把 novelty 完全压在 source 端。这样设计有几个收益:
- baseline 对照实验干净:默认 CodeQL 与 ToLO-aware 规则的差异完全来自 source 定义,而不是混杂着新 sink。
- 规则解释更容易:报告可以说”这是传统 code injection sink,但 source 是 LLM output”,而不是重新命名所有危险 API。这样既尊重已有 CWE,又突出 ToLO 的 source 端贡献。
- 维护成本低:CodeQL 标准库的 sink 规则一直在更新(新增 SDK、新增危险 API),ToLO 不需要重新发明这部分。
Sink 的现实样子
初学者可以把 sink 集合理解成”真实世界会被影响的操作”:
- 执行代码(
eval/exec/compile/PythonREPL) - 跑命令(
subprocess shell=True/os.system/os.popen) - 查数据库(
cursor.execute/pandas.read_sql/ SQLAlchemytext) - 读写文件(
open/Path.read|write/shutil) - 发网络请求(
requests/httpx/urllib) - 渲染模板(Jinja2
Template/Environment.from_string) - 恢复对象(
pickle.loads/yaml.load/torch.load)
这些操作本身不一定错;错在它们接收了未受控的 LLM 输出。
七类 sink 的危险参数位置
sink 谓词最好精确到危险参数位置,而不是只标函数名:
| 子类 | 典型危险参数 | 不是重点的参数 |
|---|---|---|
ToLO-Deser | pickle.loads(data) 的 data;torch.load(path) 的 path 或载荷来源 | 固定的安全选项参数 |
ToLO-Exec | eval(code) / exec(code) 的代码字符串 | 固定 globals 中的普通常量 |
ToLO-Shell | os.system(cmd) 的命令字符串;subprocess.run(cmd, shell=True) 的 cmd | timeout、cwd 等固定配置,除非也被 LLM 控制且影响安全边界 |
ToLO-SQL | execute(query) 的 query 结构字符串;text(sql) 的 SQL 字符串 | 参数化调用中的 value tuple |
ToLO-Path | open(path)、Path(path).read_text()、shutil.copy(src, dst) 的路径参数 | encoding 等固定选项 |
ToLO-SSRF | requests.get(url)、httpx.post(url) 的 URL、host 或 scheme | 固定 header 值,除非用于转发凭据 |
ToLO-Template | Environment.from_string(template) 的模板字符串;不可信 render context 中会触发执行语义的字段 | 固定模板里的普通显示变量,视上下文而定 |
这个表的作用是减少 false positive。比如 cursor.execute("SELECT * FROM t WHERE id=%s", (llm_id,)) 中,LLM 值进入的是参数值位置,不是 SQL 结构位置;它通常不应报 ToLO-SQL。
Sanitizer 集合 C_SAFE(五类)
C_SAFE^{schema}
Pydantic Strict 模式 + Literal / Enum / 受约束类型;JSON Schema 的 additionalProperties: false + 枚举或正则字段。
可识别代码模式:
# Pydantic v2class X(BaseModel): model_config = ConfigDict(strict=True, extra="forbid") action: Literal["a", "b"] name: Annotated[str, StringConstraints(pattern=r"^[a-z]+$")]
# JSON Schema{ "type": "object", "additionalProperties": False, "properties": { "action": {"type": "string", "enum": ["a", "b"]}, },}C_SAFE^{allowlist}
显式取值允许列表:
# Tool name allowlistif tool_name not in ALLOWED_TOOLS: raise
# Path root jail (先 resolve 再 is_relative_to)target = (ROOT / user_path).resolve()if not target.is_relative_to(ROOT): raise
# URL scheme + host allowlistu = urlparse(url)if u.scheme not in {"http", "https"}: raiseif u.hostname not in ALLOWED_HOSTS: raise
# SQL table mappingreal_table = ALLOWED_TABLES[llm_table]if llm_column not in ALLOWED_COLUMNS[llm_table]: raiseC_SAFE^{parameterized}
参数化下游调用:
# SQLcursor.execute("SELECT * FROM t WHERE id = %s", (uid,)) # ✓
# Shellsubprocess.run(["git", "log", branch], shell=False) # ✓
# HTTPrequests.get(BASE_URL, params={"q": q}) # ✓
# Templateenv = SandboxedEnvironment(autoescape=True)tpl = env.from_string(FIXED_TEMPLATE) # ← 模板字符串固定tpl.render(name=llm_value) # ✓C_SAFE^{safe-codec}
安全编解码:
json.loads(x) # 替代 pickle.loadsyaml.safe_load(x) # 替代 yaml.loadast.literal_eval(x) # 替代 eval (literal)numexpr.evaluate(x, global_dict={}, local_dict={...}) # 替代 eval (math)torch.load(path, weights_only=True) # 替代 torch.load (default)C_SAFE^{capability}
能力门控:执行 LLM 指定操作前检查是否在当前会话 capability 集合中(参考 IsolateGPT、CaMeL 的设计)。
# 会话级 capabilityclass Session: allowed_tools: set[str] allowed_paths: list[Path] allowed_hosts: set[str]
# 在 sink 前检查if tool_name not in session.allowed_tools: raise PermissionError()
# 容器化 sandboxdocker.containers.run(..., network_disabled=True, read_only=True, ...)
# 最小权限数据库账号RO_DB = create_engine("postgresql://readonly_user@db/app")主流框架几乎缺失 —— 这一类目前在检测端没有稳定信号可识别;defense 端建议引入,但静态规则难以验证是否真的封住了什么。
Sanitizer 必须类型匹配
Sanitizer 不是越多越好,而是必须能切断相应 sink 的语义风险。
yaml.safe_load可以处理反序列化风险,但它解出的字符串仍可继续污染 SQL 或 shell。subprocess.run([...], shell=False)能降低 shell injection,但不能证明命令本身被授权。SandboxedEnvironment能限制模板执行能力,但如果模板字符串来自 LLM,仍需确认 sandbox 是否覆盖目标风险。
Type-matched sanitizer 矩阵
下面是规则设计时可以采用的保守矩阵。候选表示需要看具体代码;通常无效表示不应默认作为 barrier。
| Sanitizer 类 | Deser | Exec | Shell | SQL | Path | SSRF | Template |
|---|---|---|---|---|---|---|---|
C_SAFE^{schema} | 候选:对象形状固定且无自由对象实例化 | 候选:代码语言被收窄为枚举动作 | 候选:命令/工具枚举 | 候选:表名/列名枚举 | 候选:文件 ID 枚举 | 候选:host/scheme 枚举 | 候选:模板 ID 枚举 |
C_SAFE^{allowlist} | 候选:codec/type allowlist | 候选:操作 allowlist | 候选:可执行文件和子命令 allowlist | 候选:表/列/语句类型 allowlist | 候选:resolve 后 root jail | 候选:scheme/host/IP range allowlist | 候选:固定模板集合 |
C_SAFE^{parameterized} | 通常无效 | 通常无效 | 候选:list-form 且 executable 固定 | 候选:值位置参数化 | 候选:文件 ID 映射比字符串路径更强 | 候选:固定 base URL + params | 候选:固定模板 + context 值 |
C_SAFE^{safe-codec} | 候选:json / safe_load 替代危险反序列化 | 候选:literal_eval 替代 eval | 通常无效 | 通常无效 | 通常无效 | 通常无效 | 候选:sandboxed renderer,需看模板来源 |
C_SAFE^{capability} | 候选:禁止对象恢复能力 | 候选:隔离执行能力 | 候选:命令、网络、文件能力 | 候选:只读 DB/RBAC | 候选:会话路径能力 | 候选:网络出口能力 | 候选:模板执行能力 |
静态规则可以先只承认高置信 barrier:枚举、固定 ID 映射、SQL 参数化、路径 resolve 后 root 检查、URL scheme/host allowlist、固定模板名、固定 executable 的 list-form subprocess。复杂 sandbox 或自定义权限系统应进入人工 review,不要轻易自动消警。
好 barrier 与坏 barrier
每个 sink 一段对照,看清”看起来像 sanitizer”和”真正切断风险”的差别。
ToLO-Path
# bad: 字符串替换不等于路径边界path = llm_path.replace("../", "")return Path(BASE / path).read_text()
# good: 规范化后检查 root jailtarget = (BASE / llm_path).resolve()if not target.is_relative_to(BASE.resolve()): raise PermissionError()return target.read_text()ToLO-Shell
# bad: LLM 仍控制完整命令cmd = shlex.quote(llm_cmd)subprocess.run(cmd, shell=True)
# good: executable 固定,参数位置有限if branch not in ALLOWED_BRANCHES: raise ValueError()subprocess.run(["git", "log", branch], shell=False)ToLO-SQL
# bad: 字段控制 SQL 结构sql = f"SELECT * FROM {table}"cursor.execute(sql)
# good: 结构来自 allowlist,值走参数化real_table = ALLOWED_TABLES[table_id]cursor.execute(f"SELECT * FROM {real_table} WHERE id = %s", (row_id,))这里的 f"SELECT * FROM {real_table}" 只有在 real_table 来自固定 allowlist 后才可接受。否则它仍是 SQL 结构拼接。
ToLO-SSRF
# bad: 只检查字符串前缀if url.startswith("https://"): requests.get(url)
# good: 解析 URL 后限制 scheme 和 hostu = urlparse(url)if u.scheme != "https" or u.hostname not in ALLOWED_HOSTS: raise PermissionError()requests.get(u.geturl())ToLO-Template
# bad: 模板字符串来自 LLMtpl = env.from_string(llm_template)return tpl.render(context)
# good: 模板固定,LLM 只进入普通变量位置tpl = env.get_template("summary.html")return tpl.render(summary=llm_text)ToLO-Deser / ToLO-Exec
# bad: 解析后立刻进入危险执行code = json.loads(llm_text)["code"]exec(code)
# good: LLM 只能选择固定动作action = json.loads(llm_text)["action"]if action not in {"summarize", "classify"}: raise ValueError()HANDLERS[action]()安全 codec 只能说明”没有使用危险 codec / evaluator”;它不能自动证明解出的字段对其他 sink 安全。
从集合到谓词
可以把本页内容压成四个判断函数:
isLLMSource(n) := n 属于 S_LLM^{direct} OR n 属于 S_LLM^{framework} OR n 是前两类经 parser / structure / RAG 传播得到的字段
isDangerousSink(n, kind) := n 是七类 ToLO sink 的危险参数位置 AND kind ∈ {Deser, Exec, Shell, SQL, Path, SSRF, Template}
isTypeMatchedSanitizer(n, kind) := n 属于 C_SAFE^{schema, allowlist, parameterized, safe-codec, capability} AND n 对 kind 对应 sink 的语义风险有效
isToLOFinding(src, sink, kind) := isLLMSource(src) AND isDangerousSink(sink, kind) AND hasTaintPath(src, sink) AND NOT exists guard on path: isTypeMatchedSanitizer(guard, kind)实现时 isTypeMatchedSanitizer 往往拆成多个小谓词,例如 isSqlValueParameterization、isPathRootJail、isUrlHostAllowlist。这样比一个巨大的 isSanitizer 更容易测试,也能解释每条告警为什么没有被消掉。
三个容易混淆的判断
| 代码行为 | 是否 sanitizer | 原因 |
|---|---|---|
json.loads(llm_text) | 通常不是 | 只解析结构,不限制字段语义 |
if tool not in ALLOWED: raise | 通常是 | 明确限制工具集合(C_SAFE^{allowlist}) |
print(llm_text) | 不是 sink | 只是展示,不是敏感操作 |
len(text) < 1000 | 不是 sanitizer | 长度对绝大多数 sink 无效 |
re.match(r"^[a-z]+$", x) | 可能是(允许字符严格) | 对 SQL 表名 / shell exec 名有效 |
text.replace("'", "''") | 错配,不算 sanitizer | SQL escape 在现代驱动里不安全,要用参数化 |
html.escape(text) | 只对 HTML XSS 是 sanitizer | 对 SQL / shell / path / URL 不算 |
ToLO 谓词(集合视图)
一个程序点对 (p_src, p_sink) 触发 ToLO,当且仅当存在污点路径 π: p_src ↝ p_sink,满足:
p_src ∈ S_LLM^{direct, framework, parsed, structured, rag} ANDp_sink ∈ ToLO-{Deser, Exec, Shell, SQL, Path, SSRF, Template} 对应 sink ANDπ 上不存在中间节点 q ∈ C_SAFE 对相应数据做了 类型匹配 的清洗“与 sink 类型匹配”是必要的 —— 对 SQL sink 用 html.escape 不算 sanitize;对路径 sink 用 SQL parameterization 不算 sanitize。
详细的传播规则、容器传播、字符串变换、跨函数 propagation 见 Predicate Rules。
告警解释模板
静态分析报告建议按四段展示:
┌──────────────────────────────────────────────────────┐│ ⚠ ToLO-Path 告警 │├──────────────────────────────────────────────────────┤│ Source: app/agent.py:42 ││ <S_LLM^framework> AIMessage.tool_calls[0]["args"] ││ ││ Path: ││ 42 → 43: args = json.loads(message.tool_calls[0]["args"]) ││ 43 → 44: path = args["path"] ││ 44 → 47: target = Path(path) ││ ││ Sink: app/agent.py:47 ││ <ToLO-Path> Path(...).read_text() ││ 对应被保护对象: 文件系统 ││ ││ Missing guard: ││ 无 C_SAFE^allowlist (路径未经 resolve + is_relative_to 检查) ││ 无 C_SAFE^capability (会话权限未检查) ││ ││ 推荐修复: ││ - 引入 Path((ROOT / path).resolve()).is_relative_to(ROOT) ││ - 或者把 path 改为 Literal[...] 文件 ID 映射 │└──────────────────────────────────────────────────────┘这个模板能避免报告变成单纯 API 列表,也方便和公开 CVE 案例互相校验。
读完检查
判断下面路径:
S_LLM^{structured}.name -> f"SELECT * FROM {name}" -> db.execute(...)如果 name 只是 Pydantic str 字段,没有表名 allowlist,这仍然是 ToLO-SQL 候选路径。结构化输出不是自动 sanitizer。
如果 name 是 Literal["users","orders"],这才是 C_SAFE^{schema} 加上隐式 allowlist —— 安全。
下一步阅读
- ToLO 谓词与传播规则:把上面集合写成数据流谓词,详细处理传播步骤。
- Query Design Notes:把规则规格落到 CodeQL / Semgrep 实现。