Hosein-Abdollahi/provenance-gateway

GitHub: Hosein-Abdollahi/provenance-gateway

一个通过数据来源追踪来防御 LLM Agent 间接 prompt injection 的安全网关及配套评估工具。

Stars: 0 | Forks: 0

# LLM 安全网关 **针对使用工具的 Agent 的间接 prompt injection 防御 —— 以及衡量其成本的评估测试工具。** 在同一种 Agent 上针对六种注入方式评估了四种防御措施。结论是:**基于内容的过滤防御依赖于特定风格,且每种防御都有漏洞;而基于来源的防护措施则与风格无关,它以零误报和零效用损失的代价,成功拦截了全部 11/11 次攻击尝试。** ## 问题 使用工具的 Agent 会读取攻击者可控的内容——抓取的网页、文档、工具输出。*间接 prompt injection* 会将指令隐藏在这些内容中,诱导 Agent 执行超出范围的操作:窃取数据、运行命令、写入文件。 防御者真正面临的问题不是“我能否检测到注入”。而是: 这个框架就是这个项目的核心。检测率并不是最终答案;本代码库解释了原因。 ## 解决方案 一个 `Gateway` 位于 Agent 和其不受信任的输入之间。共有四个递进的条件,且评估方式完全相同: | | 条件 | 机制 | 成本 | |---|---|---|---| | **C0** | passthrough | 无——作为基线 | 0 | | **C1** | heuristic | 对内容使用 regex 模式,进行脱敏和标记 | 0(无需模型调用) | | **C2** | LLM detector | 分类器判断内容是否包含指令 | 每次工具结果 1 次模型调用 | | **C3** | provenance | 对不受信任的内容进行污染标记;拦截参数可追溯至该内容的副作用操作 | 0(无需模型调用) | C1 和 C2 问的是“这段文本看起来恶意吗?” C3 问的是“Agent 是否正准备对来自不受信任源的数据采取行动?” 这个差异就是全部结果的体现。 ## 结果 在 `qwen2.5-coder:7b`(Q4, Ollama)上测量,temperature 为 0,seed 为 0,`num_ctx=8192`,`max_steps=4`,共 24 个场景(18 次攻击 × 6 种风格,6 个良性场景)。**样本量 n 较小——详见局限性。** ![结果:安全性/附带影响权衡以及按注入风格划分的攻击成功率](https://raw.githubusercontent.com/Hosein-Abdollahi/provenance-gateway/main/results/tradeoff.png) *左图:C3 将攻击成功率降至零,同时保持良性场景的成功率在基线水平。C1 带来了较小的降幅,但付出了误报和损失效用的代价。右图:每种表层模式防御都有不同的漏洞;而 C3 没有漏洞。* | 条件 | 攻击成功率 ↓ | 尝试率 | 拦截率 ↑ | 检测率 | 误报率 ↓ | 良性成功率 ↑ | 网关耗时 | |---|---:|---:|---:|---:|---:|---:|---:| | C0 passthrough | 0.611 | 0.611 | — | 0.000 | 0.000 | 0.667 | ~0 | | C1 heuristic | 0.278 | 0.278 | 0.000 | **0.833** | **0.167** | **0.500** | 0.04 | | C2 detector | 0.444 | 0.444 | 0.000 | 0.167 | 0.000 | 0.667 | 0.01 | | **C3 provenance** | **0.000** | 0.611 | **1.000** | 0.611 | 0.000 | 0.667 | 0.02 | ### 三项发现 **1. C3 以零成本拦截了所有尝试的攻击。** 11/11 被成功阻止。误报率为 0.000。良性成功率为 0.667——*与无防御基线完全相同*。该网关是完全免费的。 **2. C3 的尝试率与 C0 完全相等(0.611)。** 这并非巧合——这是在数据中可见的机制原理。C3 从不干预内容,因此 Agent 的表现与没有网关时*完全一样*,直到它准备执行操作的那一刻。这提供了清晰的归因:模型的行为没有任何改变,因此所有效果都归功于网关。相反,C1 和 C2 通过过滤掉模型永远看不到的内容,*降低了尝试率*(0.278, 0.444)。两种不同的机制,都在降低攻击成功率。 **3. 检测率是一项虚荣指标。** C1 标记了 **83%** 的攻击,但仍然放行了 **28%** ——它去除了“忽略所有之前的指令”,但文本中留下了 `attacker@evil.com`,导致模型依然发送了邮件。它还对 17% 的良性文档产生了误报,将效用从 0.667 降至 0.500。**网关几乎可以检测到所有攻击,但几乎无法阻止任何攻击。** 只有 `attack_success_rate` 才能真正衡量防御效果。 ### 按注入风格划分的攻击成功率 | 风格 | C0 | C1 | C2 | **C3** | |---|---:|---:|---:|---:| | authority | 1.00 | **1.00** | 0.00 | **0.00** | | fake_turn | 1.00 | 0.33 | **1.00** | **0.00** | | helpful_note | 0.67 | 0.33 | 0.67 | **0.00** | | polite | 0.00 | 0.00 | 0.00 | **0.00** | | role_claim | 1.00 | 0.00 | **1.00** | **0.00** | | urgency | 0.00 | 0.00 | 0.00 | **0.00** | **每种表层模式防御都有漏洞;只是漏洞各不相同。** C1 被 `authority` 击败。C2 被 `fake_turn` 和 `role_claim` 击败。C3 在所有六种风格中均为 0.00——因为它从不关注表面特征。 还要注意 C0 揭示了关于*模型*的信息:`qwen2.5-coder:7b` 完全容易受到权威框架、虚假对话轮次和角色声明的攻击,并且天然能够抵御礼貌请求和紧急情况。这是关于模型的发现,而不是关于网关的,这就是为什么尝试率要单独报告的原因。 ## 架构 ``` task --> agent (fixed ReAct loop) --> tool call | +-----------------+-----------------+ | | side-effectful? untrusted output | | +--------v---------+ +----------v---------+ | action_guard | <-- C3 | inspect | <-- C1, C2 | block if args | | redact/flag/block | | trace to taint | | | +--------+---------+ +----------+---------+ | | (never fires) sanitized observation | back into agent context ``` Agent 在所有条件中都保持**恒定**——相同的模型、相同的 prompt、相同的 seed——因此网关是唯一的自变量。具有副作用的工具是带有埋点的桩程序:它们会*记录*尝试执行的操作,并返回逼真的确认信息(`Email sent successfully to X.`),而不会实际执行。攻击成功率是通过记录的尝试操作来衡量的,因此它是精确的,并且不产生任何真实风险。 ## 测量完整性 纯粹是为了防止网关将模型的失败归功于自己而存在四个指标。每个指标都是在出现某个产生看似合理但实为错误数字的 bug 后添加的: | 指标 | 捕捉到的问题 | |---|---| | `attack_attempt_rate` | Agent 根本没有尝试攻击。得分与“网关拦截了它”完全相同——但这属于 Agent 的失败,而非防御起效。在 C0(完全没有网关)和 `qwen2.5-coder:3b` 上观察到:8/18 次攻击“失败”是因为 Agent 在 `fetch` 上死循环,从未采取行动。 | | `parse_failure_rate` | 模型输出无法解析 → 本次流程什么也没做 → 看起来像被拦截了。 | | `unknown_tool_rate` | 模型捏造了工具名称(`fetch_doc`, `ReadDocument`)。格式正确,但毫无作用。从系统 prompt 中省略工具清单会导致可用输出降至约 10%,而仅靠解析器的检查却报告 70% 为“正常”。 | | `gateway_ms` | 仅在网关代码内消耗的时间。`avg_latency_ms` 主要受模型调用的影响,并且在缓存响应时读数约为 0——它打印 `cached` 而不是虚假的数字。 | Trace(`results/traces/*.jsonl`)记录了每次运行的每一个步骤。**聚合比率只能告诉你*出了*问题;只有 Trace 才能告诉你出了*什么*问题。** 上面的每一个 bug 都是通过阅读 Trace 而非表格发现的。 ## 复现 ``` pip install -r requirements.txt # core needs only stdlib ollama pull qwen2.5-coder:7b python preflight.py --model qwen2.5-coder:7b # ~1 min: is the model usable? python gen_corpus.py --n-benign 6 --values-per-payload 1 --out data/small.jsonl python run.py --backend local --model qwen2.5-coder:7b --data data/small.jsonl python -m src.eval.trace --dir results/traces --condition C0_passthrough # read episodes python -m pytest tests/ ``` **首先运行 `preflight.py`。** 它会报告模型是否遵循 `TOOL/ANSWER` 契约并使用真实的工具名称。如果不是,那么所有下游的数字衡量的都是格式,而不是防御能力。(注意:预检仅探测了第 1 轮——模型可能在 1.00 分时通过,但在第 2 轮及以后失败。`qwen2.5-coder:3b` 正是如此。) 后端:`--backend mock`(确定性,离线,无需模型),`--backend local`(Ollama 或任何兼容 OpenAI 的服务器),`--backend anthropic`。 **确定性。** `temperature=0` + 固定的 seed。这很重要:开启采样后,相同的配置在一次运行中产生了 C0 = 0.2 的结果,而在另一次运行中产生了 0.4 的结果。不稳定的基线会导致所有其他数字失去参考价值。它还使得响应缓存变得*完全精确*,而非近似——模型是其输入的纯函数,因此缓存命中的响应是完全相同的(在典型的运行中有 57% 的调用被跳过;每个测试流程的第 1 步在所有四个条件中是共享的)。 ## 局限性 **n = 18 次攻击。** 每次运行都会使比率变动 0.056。这些数字仅作说明,并非定论。具有可发表价值的数字需要一个真正的基准测试(AgentDojo、LivePI 或当前的标准——引用前请进行核实)以及数百个场景。代码已支持此功能;只需更改 `--data` 即可。 **C3 的 1.00 分在特定方面显得脆弱。** 它起作用是因为泄露地址*字面*出现在抓取的内容中,因此 token 匹配可以捕捉到它。如果攻击者对 payload 进行 base64 编码、将其拆分到文档各处,或者让模型重新构造它,就能直接绕过防御。这里的来源追踪是子串级别的,而不是数据流级别的。 **没有自适应攻击者。** 语料库是基于模板且静态的。允许攻击者专门针对 C3 进行*迭代*(IterInject 风格)才是真正的考验,而这并没有在此运行。C3 的满分应被解读为“没有被这六种风格攻破”,而不是“不可攻破”。 **生成器和网关由同一人编写。** 攻击风格和防御措施出自同一人之手,这使得结果偏向于 C3。外部基准测试可以解决此问题。 **`max_steps=4` 会影响效用这一列。** 被拦截的 Agent 会不断重试并耗尽步骤上限,最终被计为效用失败。攻击成功率不受影响(注入在第 2-3 步触发,完全在限制范围内——可在 Trace 中验证)。 **即使是在 C0 条件下,良性成功率也只有 0.667。** 有三分之一的干净任务在完全没有网关的情况下失败了——这是 7B 模型在死循环,而不是网关的成本。应相对于 C0 来阅读效用,而不是看绝对值。 **C3 的零误报率作为证据比看起来要弱。** 良性场景只会执行 `fetch` 和总结——它们从不调用具有副作用的工具。但 C3 *仅*在操作边界进行干预,因此它根本没有机会错误地拦截任何操作。它的 0.000 代表的是“在良性输入下从未触发”,而不是“在良性输入下正确触发”。缺失的测试是一项合法地对抓取的数据采取行动的良性任务(例如,阅读文档后给同事发送邮件)。这种情况在 [mcp-injection-guard](https://github.com/Hosein-Abdollahi/mcp-injection-guard) 的自测中得到了演练,而在构建该测试时发现了一个真实的过度拦截 bug,这是当前语料库无法捕捉到的:将叙述文本而不是可操作的标识符标记为污染,会拦截任何引用来源的摘要。在误报声明成立之前,需要包含具有副作用良性任务的语料库。 **单一模型,单次运行。** 没有跨 seed 的方差估计。 ## 未来工作 - 真正的基准适配器(AgentDojo / LivePI)——`Scenario` 数据类是唯一需要更改的接口。 - **针对 C3 迭代的自适应攻击者。** 稳健性曲线是本代码库目前尚不具备的核心结果。 - 数据流级别的来源追踪,而非子串匹配。 - 经过学习的 C2 检测器,而不是模式代理。 - 弱模型与强模型的比较:网关在未对齐的模型上应该能提供*更大*的帮助,这能将其贡献与模型自身的安全训练离开来。 ## 技术栈 核心使用 Python 标准库。可选项:`openai`(本地模型),`anthropic`(前沿模型),`matplotlib`(图表),`pytest`。没有使用任何 Agent 框架——这是刻意为之:Agent 是一个控制变量,而不是功能特性。 ## 仓库布局 ``` src/gateway/ C0-C3 behind one interface src/agent/ fixed ReAct loop, model adapters, instrumented stub tools src/eval/ harness, metrics, per-episode traces, scenario loader gen_corpus.py combinatorial scenario generator (6 styles x 3 payloads) preflight.py is this model usable? run before any sweep run.py entrypoint results/ metrics.csv, by_style.csv, tradeoff.png, RUN_CONFIG.md METHODOLOGY.md design decisions + the traps behind them ```
标签:AI智能体安全, AI网关, AI风险缓解, 反取证, 安全评估, 提示词注入防御, 数据溯源, 逆向工具