Skip to content

Sources and Sinks

ToLO 检测建立在标准污点分析框架上:三个谓词集合(source、sink、sanitizer)加传播规则。本节给出三个集合的具体划分,可直接映射到 CodeQL DataFlow::ConfigurationTaintTracking::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 语义匹配。

这一页的结构

  1. 先修概念:污点传播
  2. Source 集合 S_LLM(五子集)+ 每子集的代码模式
  3. Source 建模优先级
  4. 子集互相转化
  5. 为什么 Pydantic 不构成 sanitizer
  6. Sink 集合 S_DANGER(七类)+ 复用 CodeQL 标准库 + 危险参数位置
  7. Sanitizer 集合 C_SAFE(五类)+ 类型匹配判定 + 矩阵
  8. 好 barrier 与坏 barrier(每个 sink 一段对照)
  9. 从集合到谓词
  10. 三个容易混淆的判断
  11. ToLO 谓词(集合视图)
  12. 告警解释模板

先修概念:污点传播

污点传播可以理解为给数据贴标签:

  • 某个值来自不可信来源,就贴上 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 Pythonresponse.choices[0].message.content
OpenAI Pythonresponse.choices[0].message.tool_calls[i].function.arguments
OpenAI Pythonresponse.choices[0].message.function_call.arguments
OpenAI Pythonresponse.choices[0].delta.content(streaming)
OpenAI Pythonresponse.output_text(新 Responses API)
Anthropic Pythonresponse.content[i].text(text blocks)
Anthropic Pythonresponse.content[i].input(tool use blocks)
Google GenAIresponse.text / response.candidates[0].content.parts[0].text
Google GenAIresponse.candidates[0].content.parts[i].function_call.args
Cohereresponse.text
Mistralresponse.choices[0].message.content
Ollama / OpenAI 兼容同 OpenAI 字段

CodeQL 建模思路:把上述 SDK 的客户端方法返回值的 .content / .text / tool-call arguments 等属性访问标为 source。需要识别 SDK 类型(openai.OpenAIanthropic.Anthropicgoogle.generativeai.GenerativeModel 等)。

不要这样建模:

isSource(n) := n 的类型是 str

这会把配置文件、日志字符串、固定 SQL 片段全部污染,导致 false positive。ToLO 的 source 必须能说明它来自 LLM 或 LLM-influenceable 框架数据。

S_LLM^{framework} —— 编排框架包装后的对象字段

编排框架把模型输出包装成对象后的字段

框架字段路径
LangChainAIMessage.content
LangChainAIMessage.tool_calls[i]["args"]
LangChainAIMessage.additional_kwargs
LangChainAgentAction.tool_input(dict or str)
LangChainAgentFinish.return_values["output"]
LangChainChatGeneration.text
LlamaIndexResponse.response
LlamaIndexChatResponse.message.content
LlamaIndexAgentChatResponse.response
HaystackAnswer.data / GeneratedAnswer.data
AutoGenAgent 消息列表内 content
CrewAITask.output.raw / result
Semantic KernelFunctionResult.value
LiteLLM与 OpenAI 同接口
MCPtools/call 返回的 content[i].text

CodeQL 建模思路:为每个框架的 message / generation / agent action / response 类建立 type model,然后把这些类的 content / output / args 属性访问标为 source。

框架层 source 的常见难点是字段名看起来很普通contentvalueoutput 在普通应用里不一定危险;只有当它们属于 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 outputclient.beta.chat.completions.parse(response_format=X) → 实例字段
Anthropic tool useresponse.content[i].input(dict)
LangChain with_structured_outputllm.with_structured_output(X).invoke(...) → 实例
Instructorclient.chat.completions.create(response_model=X) → 实例
Outlinesoutlines.generate.json(model, X) → 实例
Marvinmarvin.cast(text, target=X) → 实例

重点:实例的所有字段默认都是 tainted,即使 schema 限制了形状。详见 §“为什么 Pydantic 不构成 sanitizer”。

判断结构化输出时,先问两个问题:

  1. 这个对象是否由 LLM 输出解析而来?
  2. 当前字段是否被收窄到安全枚举、正则子语言或固定能力集合?

第一问决定它是不是 source。第二问只决定某个字段能不能在特定 sink 前作为 sanitizer 候选。不要把整个 model 一次性标成已清洗

S_LLM^{rag} —— RAG 检索器返回的文档内容字段

框架字段路径
LangChainDocument.page_content
LangChainDocument.metadata(也是 attacker-influenceable)
LlamaIndexNodeWithScore.node.text
LlamaIndexNodeWithScore.node.metadata
HaystackDocument.content / Document.meta
任意 vector storevector_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.contentAgentAction.tool_input
parsed从输出解析出来json.loads(ai.content)["path"]
structured结构化输出字段Pydantic model 的字段
rag检索返回内容Document.page_contentmetadata

不要被符号吓到。它们只是说明:LLM 相关的不可信数据不只在一个对象里,而是在多个框架层之间流动

为什么 Pydantic 不构成 sanitizer

这一点经常被误解,单独说明

Pydantic 只验证类型结构(strstr、字段存在、长度在范围内),它不验证内容语义。一个被验证为合法 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-Deserpython/unsafe-deserialization
ToLO-Execpython/code-injection
ToLO-Shellpython/command-line-injection
ToLO-SQLpython/sql-injection
ToLO-Pathpython/path-injection
ToLO-SSRFpython/server-side-request-forgery
ToLO-Templatepython/template-injection(部分需自建)

JS / TS 等其他语言的 sink 集合复用同样的 CodeQL 标准库。

设计原则:Sink 复用

Sink 端不发明,把 novelty 完全压在 source 端。这样设计有几个收益:

  1. baseline 对照实验干净:默认 CodeQL 与 ToLO-aware 规则的差异完全来自 source 定义,而不是混杂着新 sink。
  2. 规则解释更容易:报告可以说”这是传统 code injection sink,但 source 是 LLM output”,而不是重新命名所有危险 API。这样既尊重已有 CWE,又突出 ToLO 的 source 端贡献
  3. 维护成本低:CodeQL 标准库的 sink 规则一直在更新(新增 SDK、新增危险 API),ToLO 不需要重新发明这部分。

Sink 的现实样子

初学者可以把 sink 集合理解成”真实世界会被影响的操作”:

  • 执行代码(eval / exec / compile / PythonREPL)
  • 跑命令(subprocess shell=True / os.system / os.popen)
  • 查数据库(cursor.execute / pandas.read_sql / SQLAlchemy text)
  • 读写文件(open / Path.read|write / shutil)
  • 发网络请求(requests / httpx / urllib)
  • 渲染模板(Jinja2 Template / Environment.from_string)
  • 恢复对象(pickle.loads / yaml.load / torch.load)

这些操作本身不一定错;错在它们接收了未受控的 LLM 输出

七类 sink 的危险参数位置

sink 谓词最好精确到危险参数位置,而不是只标函数名:

子类典型危险参数不是重点的参数
ToLO-Deserpickle.loads(data)data;torch.load(path)path 或载荷来源固定的安全选项参数
ToLO-Execeval(code) / exec(code) 的代码字符串固定 globals 中的普通常量
ToLO-Shellos.system(cmd) 的命令字符串;subprocess.run(cmd, shell=True)cmdtimeoutcwd 等固定配置,除非也被 LLM 控制且影响安全边界
ToLO-SQLexecute(query) 的 query 结构字符串;text(sql) 的 SQL 字符串参数化调用中的 value tuple
ToLO-Pathopen(path)Path(path).read_text()shutil.copy(src, dst) 的路径参数encoding 等固定选项
ToLO-SSRFrequests.get(url)httpx.post(url) 的 URL、host 或 scheme固定 header 值,除非用于转发凭据
ToLO-TemplateEnvironment.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 v2
class 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 allowlist
if 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 allowlist
u = urlparse(url)
if u.scheme not in {"http", "https"}:
raise
if u.hostname not in ALLOWED_HOSTS:
raise
# SQL table mapping
real_table = ALLOWED_TABLES[llm_table]
if llm_column not in ALLOWED_COLUMNS[llm_table]:
raise

C_SAFE^{parameterized}

参数化下游调用:

# SQL
cursor.execute("SELECT * FROM t WHERE id = %s", (uid,)) # ✓
# Shell
subprocess.run(["git", "log", branch], shell=False) # ✓
# HTTP
requests.get(BASE_URL, params={"q": q}) # ✓
# Template
env = SandboxedEnvironment(autoescape=True)
tpl = env.from_string(FIXED_TEMPLATE) # ← 模板字符串固定
tpl.render(name=llm_value) # ✓

C_SAFE^{safe-codec}

安全编解码:

json.loads(x) # 替代 pickle.loads
yaml.safe_load(x) # 替代 yaml.load
ast.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 的设计)。

# 会话级 capability
class Session:
allowed_tools: set[str]
allowed_paths: list[Path]
allowed_hosts: set[str]
# 在 sink 前检查
if tool_name not in session.allowed_tools:
raise PermissionError()
# 容器化 sandbox
docker.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 类DeserExecShellSQLPathSSRFTemplate
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 jail
target = (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 和 host
u = urlparse(url)
if u.scheme != "https" or u.hostname not in ALLOWED_HOSTS:
raise PermissionError()
requests.get(u.geturl())

ToLO-Template

# bad: 模板字符串来自 LLM
tpl = 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 往往拆成多个小谓词,例如 isSqlValueParameterizationisPathRootJailisUrlHostAllowlist。这样比一个巨大的 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("'", "''")错配,不算 sanitizerSQL 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} AND
p_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。

如果 nameLiteral["users","orders"],这才是 C_SAFE^{schema} 加上隐式 allowlist —— 安全。

下一步阅读