护栏与提示注入检测:OpenRouter 在请求链路上加的那道闸
假设你的应用把用户输入原样塞进 messages,某天有人在输入框里打了一句 ignore all previous instructions。这条请求会在哪一步被拦下?拦下之后你的客户端收到什么?如果没拦住,你事后能从哪里看出来它来过?
这三个问题的答案,OpenRouter 官方文档摊在 openrouter.ai/docs/guides/features/guardrails 这一组页面里。下面沿着文档描述的路径走一遍,把途中的关键字段标出来。
闸门装在转发之前,而且只看请求这一侧
先定位介入点。《Guardrails》页写明,guardrail 是组织用来约束成员和 API key 怎么用 OpenRouter 的一层设置,Security(提示注入与越狱检测)和 Sensitive Info(敏感信息检测)都是这层设置里的选项,与预算上限、模型 allowlist、provider allowlist、ZDR 强制这些并列。
介入的具体位置,两处文档说法一致:
- 《Guardrails》页讲 custom content filters 时写明,模式是在请求被转发给模型之前、对每一条用户消息在本地求值的;
- 《Prompt Injection Detection》页写明,正则模式在请求转发到模型 provider 之前于本地求值。
也就是说这道闸在 OpenRouter 侧、在把请求发给上游 provider 之前,不是在模型那边做的。
比介入点更容易被忽略的是扫描范围。《Sensitive Info Guardrail》页有一句写得很直:检测运行在请求的 input(prompt)侧,扫的是 message content、tool call arguments 和 prompt strings,不扫模型的响应。这一条值得单独记住——如果你担心的是模型把内部资料吐回给用户,这道闸管不到;它管的是「什么东西被送出去」。
命中之后:三档动作,各自发生什么
《Prompt Injection Detection》页把命中后的处置写成三档,语义各不相同:
| 动作 | 请求是否到达模型 | 内容是否被改 | 典型用途(文档写明) |
|---|---|---|---|
flag | 到达 | 原样不改 | 只记录检测结果供观测(指标与分析事件),先在自己的真实流量上测真阳性率 |
redact | 到达 | 命中片段被替换 | 命中的片段替换为 [PROMPT_INJECTION],把净化后的请求转发给模型 |
block | 不到达 | —— | 整条请求以 403 被拒,不会到模型 |
这里最容易踩的坑是 flag。文档在「Limitations」一节专门重申:flag 模式不做任何强制,被 flag 的请求会原封不动地转发给模型,检测结果只进仪表盘和分析。有人把 flag 当成「拦了但放行一部分」,那是误解——它一点都不拦。
敏感信息那一侧的替换标签不一样。《Sensitive Info Guardrail》页写明,内置预设命中并选择 redact 时,替换成带类型的占位符,例如 [EMAIL]、[PHONE];自定义正则命中并选择 redact 时,替换成 [REDACTED]。内置预设的 slug 与替换标签是一一对应的,API 侧用 content_filter_builtins 字段配置:
{
"name": "PII Protection",
"content_filter_builtins": [
{ "slug": "email", "action": "redact" },
{ "slug": "phone", "action": "redact" },
{ "slug": "ssn", "action": "block" },
{ "slug": "credit-card", "action": "block" },
{ "slug": "ip-address", "action": "redact" },
{ "slug": "person-name", "action": "redact" },
{ "slug": "address", "action": "redact" }
]
}
自定义正则走另一个字段 content_filters,每条支持 pattern、action,外加一个可选的 label——文档写明 label 的用途是在 block 时给出更清楚的错误信息。
{
"name": "Custom Filters",
"content_filters": [
{ "pattern": "AKIA[0-9A-Z]{16}", "action": "block", "label": "AWS Key" },
{ "pattern": "PROJ-\\d{4,6}", "action": "redact" }
]
}
以上为按官方文档中的字段语义组合的示例,未经实测,以官方文档与 API 的实际响应为准。
403 长什么样,以及怎么让它多说几句
被 block 的请求返回 HTTP 403 Forbidden。《Guardrails》页给出的响应体形如:
{
"error": {
"code": 403,
"message": "Request blocked: prompt injection patterns detected",
"metadata": {
"patterns": ["ignore all previous instructions"]
}
}
}
内容过滤器那一侧的 message 格式不同,是 Request blocked by content filter: [LABEL]。文档写明 [LABEL] 的取值来源:内置预设取预设名(如 Email address、Social Security number),自定义模式取你填的 label,没填 label 的自定义模式取 [BLOCKED],NLP 检出的实体则取实体类型(文档举的例子是 Blocked PII detected: PERSON)。
这里有个排查上很关键的分界:《Guardrails》页写明,预算上限和 allowlist 限制(指模型、provider 这类可用范围的 allowlist,不是后文那个提示注入豁免用的 allowlist)同样返回 403,但只有运行时的内容检查会带上 openrouter_metadata 的阶段细节。所以光看 403 判断不了是被内容拦了还是被额度/名单拦了——想分清,得把阶段信息要出来。
要阶段信息需要显式 opt in:加上 X-OpenRouter-Experimental-Metadata: enabled 这个 header(header 名里就带着 Experimental,文档把它归在 router metadata 下),403 响应会额外带一个 openrouter_metadata 对象,其中的 pipeline 数组会列出跑过的每一个 guardrail 阶段。文档示例里 pipeline 的一项是这样的:
{
"type": "guardrail",
"name": "regex_pi_detection",
"guardrail_id": "grd_abc123",
"guardrail_scope": "api-key",
"summary": "Blocked: prompt injection detected (1 pattern matched)",
"data": {
"action": "blocked",
"detected": true,
"engines": ["regex"],
"patterns": ["ignore all previous instructions"]
}
}
(上面截取的是文档 403 示例里 pipeline 数组的一项;同级还有路由相关字段,与本篇无关就不抄了。示例中的 id 值是文档的示意值。)
guardrail_id 和 guardrail_scope 是这段里最有用的两个字段——多层 guardrail 叠在一起时,它直接告诉你是哪一条、挂在哪一层拦的。完整的响应形状与阶段参考,文档指向 openrouter.ai/docs/guides/features/router-metadata 的 Error Responses 一节和 openrouter.ai/docs/api_reference/errors-and-debugging 的 Guardrail Errors 一节。
多条 guardrail 同时生效时,谁说了算
一个请求可能同时落在工作区默认 guardrail、成员 guardrail 和 API key guardrail 之下。《Guardrails》页把合并规则写死了:provider allowlist 和 model allowlist 取交集;ZDR 按模型分组走 OR(任一条 guardrail 对某个分组启用了就生效);Sensitive Info 取并集(所有适用 guardrail 的过滤器合并),同一实体类型或同一模式出现不同动作时 block 优先于 redact;预算则是各自独立检查。
提示注入这一侧的优先级在《Prompt Injection Detection》页单独写了一遍,顺序是 block > redact > flag——最严格的那档赢。所以排查「为什么这条请求被拦了」时,别只盯着你刚改的那一条 guardrail。
漏网的三个方向,文档都写了
正则拦不住换个写法。文档在正则模式之外补了几类规避检测,命中的判定不走同一套逻辑:
- Typoglycemia(乱序拼写):打乱关键词中间字母、保留首尾(文档举的例子是
ignroe),系统会检查一组目标词的这类变体; - 编码规避:先解码 Base64 和十六进制内容(文档写明包含空格分隔的十六进制对,例如
69 67 6e 6f 72 65),再在解码后的文本里查注入关键词。这一路跑两个编码检测器:base64_encoded_injection和hex_encoded_injection; - 字符间隔规避:形如
i g n o r e p r e v i o u s的文本会先折叠空格做归一化,再拿全部模式重扫一遍。
正则那一侧,文档把模式按攻击手法分成九类(从直接指令覆盖、prompt 提取、角色操纵,一直到标签注入与角色伪装、控制 token 注入),每条模式都带名字、正则和说明,例如 ignore_previous_instructions、reveal_prompt、system_tag_injection、control_token_injection。文档写明这些模式派生自 OWASP 的 LLM 提示注入防护 Cheat Sheet 等资料,除特别注明外大小写不敏感(dan_jailbreak 那条对 DAN 大小写敏感)。
误报这一侧:allowlist 有个反直觉的边界
做安全培训、做客服话术的应用,正文里天然就会出现「ignore previous instructions」这类句子。文档给的出口是 allowlist(openrouter.ai/docs/guides/features/guardrails/prompt-injection/allowlist),机制是先遮蔽再检测:先按大小写不敏感的方式在消息里定位 allowlist 短语,把命中片段替换成中性占位文本,然后在遮蔽后的文本上跑检测;如果动作是 redact,最终输出里会把 allowlist 短语还原回去,只有非 allowlist 的注入模式被换成 [PROMPT_INJECTION]。
反直觉的地方在于它的适用范围:allowlist 只对正则模式生效。文档明说,typoglycemia 和 Base64/十六进制编码这两类规避检测不受 allowlist 影响,因为它们跑在解码或归一化后的文本上,做不了有意义的按短语豁免;一旦触发这两类检测,整条消息会被 flag 或 redact,你的 allowlist 条目一个都不管用。
另外几条边界一并记下:匹配是精确子串,大小写不敏感,不支持通配符和正则;三种动作都适用;作用范围是按账号实体而非按 guardrail——个人账号是用户自己,组织里是组织本身,且文档写明组织内只有 org admin 能查看和管理 allowlist。条数上限与单条长度上限文档给了具体数字,这类阈值改起来没有成本,这里只说存在上限、具体值以官方文档为准;被停用(toggle off)的条目不计入上限。
标了 beta 的那两个预设,行为要单独记
《Sensitive Info Guardrail》页把检测分成两种方法:正则和 NLP。正则覆盖邮箱、电话、SSN、信用卡号、IP 地址这些格式化程度高的;人名和物理地址这类靠模式匹配不可靠的走 NLP 实体识别(文档写明用的是 Presidio)。
Person Name 和 Address 这两个预设,文档明确标注为 beta。 文档同时写明两件事:一是检测准确度可能有波动,尤其是不常见的人名格式和不完整、不规范的地址;二是——这条对安全设计影响最大——如果检查超时,请求会继续放行(not blocked)。也就是说依赖这两个预设做阻断,存在一条「超时即放行」的通道。文档还写明 NLP 检测会带来与输入文本长度成正比的额外延迟,这两个预设在控制台里带「Adds latency」标记。
自定义正则也有硬约束。custom content filters 用的是 JavaScript 风格正则,字符类、量词、或、非捕获组、命名捕获组、锚点、转义序列都支持;但前瞻 (?=…) / (?!…)、后顾 (?<=…) / (?<!…)、反向引用(\1、\k<name>)以及 (a+)+ 这类嵌套量词都不允许,创建和更新时会被 API 以 invalid_regex_pattern 错误拒掉。敏感信息侧的自定义模式同样做两项校验:语法必须是合法 JavaScript 正则,且不能有灾难性回溯风险(ReDoS),(a+)+、(a|a)* 这类会被拒。
想用 API 改工作区默认那条
《Guardrails》页写明每个工作区有一条默认 guardrail,不需要显式分配就对该工作区的全部流量生效。文档给的定位方式是先列出来找到它——它的名字形如 Workspace <workspace-id> Default:
curl https://openrouter.ai/api/v1/guardrails?workspace_id=YOUR_WORKSPACE_ID \
-H "Authorization: Bearer YOUR_MANAGEMENT_KEY"
拿到 id 之后用 PATCH /api/v1/guardrails/{id} 更新,文档写明全部 guardrail 设置都能这样配。注意这条命令用的是 Linux/macOS shell 的反斜杠续行写法;Windows 侧,PowerShell 的续行符是反引号、CMD 是 ^,连着反斜杠原样粘进去会报错,最省事的做法是把参数拼成一行再执行(这属于通用的 shell 差异,不是官方文档内容)。示例里的占位值换成你自己的,别把密钥写进代码仓库。
该平台迭代频繁,上面涉及的字段名、错误码与响应形状都随版本变动,以官方文档最新内容为准。
最后:这道闸能挡什么,不能挡什么
文档自己在「Limitations」里把话说得很清楚,这里照实转述,不做加工:
- 正则检测不是穷尽的,复杂或新型的注入手法可能不被捕获;
- flag 模式不执行任何强制;
- 误报是可能的,涉及安全测试之类的正常 prompt 也可能命中模式,文档建议先用
flag在有代表性的流量上跑,确认误报率可接受后再放开强制; - 把某次检测标记为 false positive 不会追溯性地放行那条请求——动作是 block 的话,原请求已经被拒了。
再叠加前面那几条:检测只跑在输入侧,不看模型响应;两个 NLP 预设是 beta 且超时会放行;allowlist 对规避检测不生效。合起来只能得出一个结论:开启 guardrail 不等于安全。它是请求链路上一层可配置的、有已知盲区的过滤,能降低已知模式的命中面、把事件收进可观测数据,但不适合当作唯一防线,也替代不了应用侧对用户输入与工具权限的设计。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。
真要动手,文档给的顺序是务实的:先用 flag 收集自己流量上的命中情况,从 Logs 页面看 guardrail 事件(文档写明带 guardrail 事件的记录行会显示 shield 图标),把明显的误报短语加进 allowlist 或标记为 false positive,再决定哪些类别升到 redact、哪些升到 block。敏感信息那一侧的建议是同一个口径:先从 Redact 起步。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。