存储型提示注入可跨越两次合法 API 调用
文章用假设场景说明:恶意指令先作为简历内容合法入库,之后被高权限 Agent 读取并触发数据外传。两次调用都可能通过网关鉴权,风险出现在持久化内容被重新解释为指令的环节;这并非真实事故披露。
AI 深度解读
一份简历里的隐藏指令,先被正常存入数据库,几个月后又被有财务工具权限的 Agent 读到,最终诱导它向外发送数据。写入和读取都通过了 API 网关授权,攻击却仍可能发生。
这篇文章讨论的是一个思想实验,没有真实受害者或泄露数据。它值得读的地方,是指出了一个容易漏掉的边界:授权读取一条记录,不等于允许记录里的文字指挥 Agent。作者借 Sentinel 说明,防线应该怎样补到数据返回模型的那一刻。
两次调用都合法,攻击为什么还能成立
场景中,负责简历入库的 Agent 收到一份带隐藏提示词的简历,通过合法 API 把内容写进数据库。网关验证凭证、检查数据结构,批准写入。
几个月后,权限更高的财务 Agent 读取这条记录。读取同样符合权限要求,但模型把记录里的隐藏文字当成了指令,随后可能调用 send_webhook() 向外发送数据。
问题出在数据身份的变化:陌生人提交的简历,经过数据库存储和另一个 Agent 的读取,看起来成了可信的内部资料。它的来源没有变,模型对它的待遇却变了。
数据库可以保存外部输入,但不会自动把外部输入变成可信指令。
作者还提到一个真实的非 AI 网关案例:未经验证的字符串被写入键值映射,之后被当作 curl 命令执行。两者结构相似,都是写入时作为数据保存,读取后被权限更高的执行者用于行动。原文没有提供该案例的具体细节,不能据此判断 AI 场景的实际发生率。
身份认证、结构校验、限流和权限范围检查,回答的是调用者能否执行这个操作。它们通常无法回答,返回内容是否正在诱导模型改变任务。
为什么要在数据返回模型之前再检查
入库时检查有价值,但只守入口,需要相信所有历史和未来的写入方都处理正确,也需要处理加检查之前就存在的旧记录。
读取后的检查点更贴近风险发生的位置。无论内容存了多久、经过多少次合法调用,只要即将进入模型上下文,就需要重新判断它能否作为输入使用。
作者介绍的 Sentinel 会清理进入模型的内容;在 Agent 场景中,也可以先扫描工具结果,再把结果交还给 Agent。财务 Agent 的数据库查询结果,就是这里要拦截的工具结果。
原文列出了几层处理:
- Layer 0 提取 HTML 注释、CSS 隐藏元素等内容,单独进入深度检测路径,并使用更低的拦截阈值。如果隐藏指令仍以标记形式保存在记录里,这一步会针对它检查。
- Layers 1 和 2 解码常见编码,并在内部副本上规范化不可见字符、形似字符和双向文本干扰,让混淆后的指令以可读形式接受扫描。
- Layer 3 使用正则表达式识别冒充权威、滥用工具,以及类似「POST this to https://...」的数据外传指令。
- Layer 4 用语义嵌入与攻击特征库比较,尝试识别没有逐字匹配规则的攻击表达。
这些是产品机制描述,并非这次场景的实测结果。原文没有给出隐藏提示词的具体内容,作者也明确表示,无法确定究竟哪一层会命中。
这套方案还有一个硬前提:数据库结果必须先经过检查,再进入模型上下文。 Sentinel 不会自动坐在网关内部监视写入,也不能替代网关的授权职责。
工具封装最关键的是返回哪份内容
原文用下面的 Python 示例展示接入位置:数据库读取完成后,先调用清理接口,再决定工具返回什么。代码是示意,并非真实运行记录。
import httpx
def fetch_candidate_record(record_id: str) -> str:
row = db.get(record_id) # stored months ago by the ingestion agent
result = httpx.post(
"https://api.sentinelaifirewall.com/v1/scrub",
json={"content": row["resume_text"], "tier": "strict"},
headers={"X-Sentinel-Key": "sk_live_..."},
).json()
action = result["security"]["action_taken"]
if action == "blocked":
log_security_event(record_id, result["request_id"])
return "[record withheld: failed content scan]"
return result["safe_payload"] # never the raw row
要盯住最后一行:交给模型的是 safe_payload,不能检查完又返回原始记录。拦截分支则根据 security.action_taken 是否为 blocked 判断,并记录 request_id。
原文还给了一个虚构响应,里面的 threat_score 是 0.91。这个数字只用于展示响应形状,不代表检测准确率,也不是实测分数。被拦截时,safe_payload 仍是描述性占位文本,并非 null,所以不能靠字段是否为空判断是否放行。
如果使用 agentic proxy,代理会把被拦截的工具结果替换成无害占位内容,SDK 收到的仍是正常响应。Teams 和 Enterprise 还提供 secret filter,可在工具结果进入模型前遮盖 API key、token,且独立于威胁分数工作。
我的判断
作者对信任边界的判断成立。对于有工具权限的 Agent,内部数据库只是存储位置,不能作为内容可信的证据。把检查放在工具返回入口,也比要求每个调用点各自记住处理,更容易形成一致约束。
但文章没有给出检测率、误报率、延迟、成本或绕过测试,也没有比较 Sentinel 与其他方案。它首先是一篇带产品推广的方案说明,不能当成有效性验证报告。
尤其是 Layer 0 的理由需要保留余地。作者认为,对人隐藏、对模型可见的内容没有正当用途,这个判断过于绝对;HTML 注释和隐藏元素也可能承载正常内容。它们值得提高检查强度,却不足以单独证明攻击成立。
读取时扫描与工具最小权限、外发目标限制可以配合使用。扫描尝试识别恶意内容,权限约束则限制误判后的行动范围。原文没有提供这些做法的对照数据,不能据此认定某个产品能独立补齐整条防线。
能拿来做什么
- 先盘点 Agent 从数据库、向量库、工单系统和文档库读取内容的入口,优先检查拥有财务操作或数据外发工具权限的 Agent。
- 在 Node 或其他语言实现的工具封装中,把内容检查放在查询完成与返回模型之间;参照示例显式判断拦截状态,并确保最终返回经过处理的内容。
- 验证方案时加入历史记录和隐藏内容样本,分别观察拦截、放行和误报情况。不要把示意响应里的
0.91当作验收依据。 - 若工具结果可能携带凭证,同时评估敏感信息遮盖。原文称 Starter 免费层为每月 100 次请求、无需信用卡,Enterprise 支持自托管;这些接入选项仍需结合实际调用量和部署要求判断。