过去一年里我给三个 LLM 应用做过安全评估,无一例外都栽在同一个地方:提示注入。 它不像 SQL 注入那样有成熟的参数化方案可以一劳永逸 —— 自然语言没有「转义」这回事, 指令和数据在模型眼里是同一种东西。
[!] TL;DR
提示注入没有银弹。规则打底、语义分类器缩小盲区、权限最小化兜底 —— 三层都要有,缺一层就是给攻击者留门。
攻击面在哪里
只要模型会读「不受你控制的文本」,攻击面就存在。常见的三个入口:
- 直接注入:用户在对话框里让模型「忽略之前的所有指令」;
- 间接注入:RAG 检索到的网页、文档里埋着恶意指令,模型替攻击者执行;
- 工具链注入:Agent 调用外部工具,返回值里带指令,污染后续决策。
其中间接注入最隐蔽 —— 受害者甚至不是发起对话的人。给模型接上浏览、邮件、代码执行能力之后,
一段藏在 <!-- 注释 --> 里的文字就可能变成一次数据外带。
一个最小检测示例
先从最朴素的规则基线说起。它的意义不是「够用」,而是给后面的语义检测提供一个可对比的锚点:
# 朴素基线,别直接上生产
import re
INJECTION_PATTERNS = [
r"(?i)ignore (all|previous) instructions",
r"(?i)you are now .+",
r"(?i)(reveal|print).*system prompt",
]
def injection_score(text: str) -> float:
hits = sum(bool(re.search(p, text)) for p in INJECTION_PATTERNS)
return hits / len(INJECTION_PATTERNS) # 召回率感人,仅作第一道闸
在我收集的 2400 条真实攻击样本上,这套规则的召回率只有 31%。换成小型分类器(微调过的
deberta-v3-small)之后召回率到 89%,误报率 2.3% —— 但代价是每次请求多 8ms 延迟。
检测:从规则到分类器
规则告诉你「已知的坏」长什么样,模型才有机会告诉你「没见过的坏」大概率长什么样。
实践中我的建议是把两者串成流水线:规则负责零成本拦截脚本小子,分类器负责语义变体, 再对高风险会话抽样送大模型复审。三层的成本差着数量级,流量漏斗决定了整体开销可控。
防御要分层
检测只是半场。就算漏过去一条注入,也要让它「拿不到东西」:
- 工具权限最小化:模型能调的每个工具都问一句「被注入时最坏会怎样」;
- 输出侧过滤:外带通常发生在输出环节,URL / 编码块要重点盯;
- 敏感操作二次确认:转账、删数据这类动作,永远不要让模型单方面完成。
完整的评测数据和流水线代码我放在了 prompt-armor 仓库里,欢迎复现和提 issue。