HTTP 响应头也是 Agent 注入入口
据文中转述,研究者扫描2.17亿条网页响应,在30个站点发现实际提示注入尝试。Claude 默认忽略这些响应头,但当用户提示赋予站点权威时,9次测试中有7次遵循了注入指令。
AI 深度解读
给 AI Agent 抓网页,只检查 HTML 正文,可能漏掉响应头里的指令。原文引用 Signals Corps 对 2.17 亿次网页响应的扫描:真正携带提示注入的网站只有 30 个,但后续测试暴露了一个更关键的问题:用户一旦让模型信任网站,原本被忽略的指令就可能生效。
这篇值得读的地方,是它提醒我们重新检查 fetch 工具交给模型的内容。不过,扫描结果、模型实验和作者推销的 Sentinel 防护方案,需要分开判断,不能把它们当成一条已经验证完整的证据链。
攻击入口为什么会藏在响应头里
HTTP 响应包含正文和响应头。开发者通常关心 HTML、JSON、Markdown,却可能把状态行、响应头一起返回给 Agent,或把部分字段包装成「元数据」。
模型看到的仍然是文本。远程服务器控制的字段,不会因为叫 metadata 就自动可信。
攻击者只需在响应头中放入类似内容:
X-AI: <instruction-shaped text addressed to the agent>
这里需要盯住的是 X-AI 后面的值:它可以是一段直接对 Agent 发号施令的文字。控制网站、被入侵的 CDN 或反向代理,都可能成为写入入口。
Signals Corps 从 Common Crawl 的 217 million 次页面响应中,发现 7,959 个网站发送面向 AI Agent 的响应头。大多数只是提供 llms.txt 或 MCP server 的位置,属于正常发现信息;30 个网站包含实际提示注入,其中 24 个使用 X-AI,多数是恶作剧,部分要求访问 shell 或数据库。
所以,按字段名一刀切也有代价。真正需要区分的是:它在提供资源地址,还是试图改变 Agent 的任务和权限。
为什么一句「听网站的」就能改变结果
原文转述的测试中,Claude 默认忽略了注入响应头;但用户提示一旦授予网站权威,模型在 9 次运行中有 7 次服从了响应头。
外部内容获得多少权威,往往取决于你如何描述它。
「参考网站资料」和「遵循网站指令」看起来接近,后者却可能让外部文本参与决定下一步操作。响应头进入上下文后,就有机会借这层授权影响工具调用。
这里有三个容易漏掉的环节。首先,只清洗正文的流水线,根本看不到响应头里的攻击。其次,响应头没有脚本标签,也不需要浏览器执行,传统网页内容检查未必覆盖这种输入。最后,模型默认拒绝只是特定测试条件下的表现,不能代替应用自己的权限边界。
但也不能把 7/9 解释成 Claude 的普遍受攻击成功率。原文没有给出具体模型版本、完整提示、采样设置和各类攻击的分布,也没有证明 shell 或数据库操作真的执行成功。它展示的是一次提示框架变化带来的行为差异。
Sentinel 的分层扫描解决哪一段问题
作者提出,在响应进入模型上下文前,用 Sentinel 同时清洗响应头和正文。但作者也明确承认:没有拿 Signals Corps 扫描到的具体响应头测试 Sentinel。下面是产品机制说明,不是该数据集上的防护成绩。
Layers 1 和 2 负责解码与规范化:处理 Base64、hex、ROT13,以及不可见字符和同形字符,再扫描解码后的内容。
Layer 3 使用快速正则,识别忽略既有指令、替换 system prompt、切换角色和滥用工具等模式。作者称其延迟接近零,但没有提供基准数据。遇到描述攻击的引用语境时,快速路径会交给评分评估,避免看到攻击示例就立即硬拦截。
Layer 4 比较语义向量与攻击特征库。standard 模式下,相似度超过 0.40 标记、超过 0.55 中和、超过 0.82 阻断;strict 模式把标记和中和阈值分别降到 0.25、0.40。中和就是移除或改写危险片段。原文没有单独说明 strict 的阻断阈值。
下面的示例展示了如何把响应头和正文一起送去清洗,示例输入并非来自扫描数据集:
import httpx
SENTINEL = "https://api.sentinelaifirewall.com/v1/scrub"
KEY = {"X-Sentinel-Key": "sk_live_..."}
def fetch_for_agent(url: str) -> str:
resp = httpx.get(url, timeout=10)
# Serialize headers the way your agent would see them
header_text = "\n".join(f"{k}: {v}" for k, v in resp.headers.items())
# Scrub headers AND body as untrusted input
scrubbed = httpx.post(
SENTINEL,
json={"content": header_text + "\n\n" + resp.text, "tier": "strict"},
headers=KEY,
).json()
action = scrubbed["security"]["action_taken"]
if action == "blocked":
return "[fetch withheld: injection detected in response]"
return scrubbed["safe_payload"]
要注意 header_text + "\n\n" + resp.text,它决定响应头是否进入检查范围;也要注意 action_taken,业务代码必须据此处理结果。
blocked 表示阻断;neutralized 的 safe_payload 已移除危险片段;flagged 则原样放行,由应用决定记录、告警还是拒绝。原文示例响应中的 threat_score: 0.91 也是演示值,不能当成实测成绩。
使用 Anthropic SDK 时,作者还提供 agentic proxy:把 base_url 指向 https://api.sentinelaifirewall.com/v1,并在 Dashboard → Settings 配置自己的 Anthropic key。代理自动扫描 tool_result,因此只有被 fetch 工具放进工具结果的响应头才会被覆盖;其他入口仍需直接调用 /v1/scrub。
我的判断
我认同它对输入边界的提醒。判断风险应沿着「外部文本如何进入上下文、获得什么权威、能触发哪些操作」追踪,正文和响应头都该纳入检查。
但扫描器的介绍不能代替效果验证。原文没有给出 Sentinel 在这批攻击上的检出率、误报率、漏报率,也没有与直接裁剪响应头做对照。正常 llms.txt 和 MCP 地址「应该通过」,仍是设计预期。
对只需要网页正文的工具,响应头白名单通常更容易审计;需要保留较多外部信息时,再评估内容扫描的收益和成本。扫描也不能代替 shell、数据库的权限控制。
Starter 提供每月 100 次免费请求,无需信用卡,适合初步试用,却不足以证明生产可靠性。示例也没处理清洗服务超时或返回异常,这些失败路径上线前必须补齐。
能拿来做什么
- 检查前端或 Node 项目中 fetch、browse 工具实际返回给模型的字符串,确认响应头是否通过包装对象或日志混入上下文。
- 对只需正文的抓取任务,裁剪无用响应头;确需保留时,使用白名单,例如
content-type,必要时保留content-length。 - 审查 Agent 提示中的「遵循网站指导」等表述,明确外部资料不能授予新的工具权限,并为 shell、数据库操作单独设置约束。
- 评估清洗服务时,同时测试攻击指令、正常发现地址、攻击引用、编码变体和服务异常,分别确认阻断、改写、告警后的应用行为。