4. 安全控制基本原则
好,咱们进入第四章。前面几章我们聊了智能体的输入侧安全,比如提示注入、数据投毒。但光防住输入就够了吗?
远远不够。你想想看,智能体最终是要输出东西的——生成一段HTML、调用一个API、写一条数据库记录。如果输出侧不加防护,攻击者照样能通过你精心训练的智能体,把恶意代码送到用户浏览器里,或者把敏感数据泄露出去。
这一章,我们就来彻底搞定输出编码与转义。说白了,就是让智能体输出的每一段文本,都“安全地”到达它该去的地方。
4.1 为什么输出编码是必须的?
我刚开始做AI安全那会儿,总觉得输出编码是前端工程师的事。直到有一次,我负责的一个客服智能体,在回复用户消息时,直接把用户输入的 <script>alert('xss')</script> 原样返回到了网页聊天窗口里。
结果呢?所有在线客服的浏览器都弹出了警告框。虽然只是个恶作剧,但如果换成窃取Cookie的脚本呢?后果不堪设想。
这就是输出编码的必要性。智能体不是生活在真空中,它的输出最终会被各种解释器消费:
- HTML解释器:浏览器会把
<script>当成代码执行 - SQL解释器:数据库会把
' OR 1=1 --当成查询语句 - JSON解析器:特殊字符会破坏数据结构,甚至触发原型链污染
- Shell解释器:
; rm -rf /这种字符串可能直接执行系统命令
核心原则:永远不要信任智能体生成的输出。把它当成一个“不可信的、可能包含恶意内容”的外部数据源来处理。
输出编码的本质,就是把特殊字符转换成它们对应的“安全表示形式”。这样解释器就不会把它们当成代码来执行了。
4.2 HTML实体编码:防XSS的第一道防线
HTML实体编码,是Web场景下最基础、最重要的输出编码手段。我个人的习惯是:只要智能体的输出要嵌入到HTML页面中,不管内容看起来多安全,一律做实体编码。
4.2.1 核心字符映射
| 原始字符 | HTML实体 | 说明 |
|---|---|---|
< |
< |
小于号,防止标签注入 |
> |
> |
大于号,防止标签闭合 |
& |
& |
&符号,防止实体注入 |
" |
" |
双引号,防止属性注入 |
' |
' |
单引号,防止属性注入 |
4.2.2 实战代码:Python中的HTML编码
在Python里,我推荐用 html 标准库,别自己写正则替换。自己写的很容易漏掉边界情况。
import html
# 智能体生成的回复
agent_reply = '<script>alert("你好")</script>'
# 安全编码
safe_reply = html.escape(agent_reply, quote=True)
print(safe_reply)
# 输出: <script>alert("你好")</script>
小技巧:quote=True 参数会把双引号也转义。如果你要把内容放到HTML属性里(比如 <div data-value="...">),这个参数必须打开。
4.2.3 避坑指南:上下文感知编码
我曾经犯过一个错:把智能体生成的URL直接放到 <a href="..."> 里,只做了HTML实体编码。结果攻击者构造了一个 javascript:alert(1) 的链接,实体编码对 javascript: 协议完全无效。
重要提醒:HTML实体编码只对 HTML标签内容 和 属性值 有效。对于URL、JavaScript代码、CSS样式等上下文,需要专门的编码方案。
- URL上下文:使用
urllib.parse.quote()进行URL编码 - JavaScript上下文:使用
json.dumps()进行JS字符串转义 - CSS上下文:使用CSS转义序列
4.3 JSON序列化安全:别让数据变成炸弹
智能体经常需要输出JSON格式的数据,用于API调用或前端渲染。JSON看起来简单,但安全陷阱不少。
4.3.1 特殊字符处理
JSON字符串中,以下字符必须转义:
"→\"\→\\/→\/(可选,但建议转义)\b→ 退格符\f→ 换页符\n→ 换行符\r→ 回车符\t→ 制表符
千万别自己手写JSON序列化。我见过有人用字符串拼接生成JSON,结果用户名字里带了个双引号,直接把整个JSON结构搞崩了。
4.3.2 使用安全的序列化库
import json
# 智能体生成的数据
agent_data = {
"user": "张三",
"message": '他说:"你好"',
"metadata": {
"source": "web",
"tags": ["ai", "security"]
}
}
# 安全序列化
safe_json = json.dumps(agent_data, ensure_ascii=False)
print(safe_json)
# 输出: {"user": "张三", "message": "他说:\"你好\"", "metadata": {"source": "web", "tags": ["ai", "security"]}}
关键参数:ensure_ascii=False 确保中文不被转义成 \uXXXX 形式,提高可读性。但如果你要传输到只支持ASCII的系统,就别用这个参数。
4.3.3 防止JSON注入与原型链污染
在Node.js环境中,JSON解析有个著名的坑:__proto__ 键名可能导致原型链污染。
// 恶意JSON
const malicious = '{"__proto__": {"admin": true}}';
// 直接解析
const obj = JSON.parse(malicious);
// 如果后续代码把obj合并到其他对象,可能导致所有对象都变成admin
防御措施:
- 解析后检查键名,过滤
__proto__、constructor、prototype - 使用
JSON.parse()的第二个参数(reviver函数)进行过滤 - 考虑使用
Object.create(null)创建无原型对象
4.4 防止数据泄露:输出侧的隐私保护
输出编码不只是防攻击,还要防泄露。智能体在生成回复时,可能会无意中把敏感信息带出来。
4.4.1 常见泄露场景
| 场景 | 风险描述 | 示例 |
|---|---|---|
| 日志输出 | 智能体把、数据库密码写入日志 | {"": "sk-xxxx"} |
| 错误信息 | 堆栈跟踪暴露了内部路径和数据库结构 | File "/app/config/db.py", line 42 |
| 个人身份信息 | 智能体在回复中包含了用户的手机号、身份证 | 您的身份证号是 110101199001011234 |
| 内部数据 | 智能体泄露了未公开的业务数据 | 下季度产品定价为 999 元 |
4.4.2 输出过滤策略
我建议在智能体的输出管道中,加一个“输出过滤器”。这个过滤器做三件事:
- 模式匹配:用正则表达式匹配常见的敏感信息模式(手机号、邮箱、密钥格式)
- 内容审查:基于关键词或语义分析,检测是否包含内部术语
- 脱敏替换:对匹配到的敏感内容进行脱敏处理
import re
def output_sanitizer(text):
# 1. 脱敏手机号
text = re.sub(r'1[3-9]\d{9}', '****', text)
# 2. 脱敏邮箱
text = re.sub(r'\w+@\w+\.\w+', '***@***.***', text)
# 3. 脱敏(假设格式为 sk-开头)
text = re.sub(r'sk-[A-Za-z0-9]{32,}', 'sk-****', text)
# 4. 检查内部关键词
internal_keywords = ['内部定价', '未公开', '机密']
for keyword in internal_keywords:
if keyword in text:
text = text.replace(keyword, '[已过滤]')
return text
# 使用示例
agent_output = "用户手机是13800138000,邮箱是test@company.com"
safe_output = output_sanitizer(agent_output)
print(safe_output)
# 输出: 用户手机是****,邮箱是***@***.***
经验之谈:输出过滤器要放在编码之前。先脱敏,再编码,顺序不能乱。我曾经搞反过顺序,结果脱敏后的 **** 被编码成了 ****,用户看到一堆乱码。
4.5 本章知识体系图
下面这张图,把输出编码与转义的核心逻辑串起来了。你可以把它当成一个检查清单:
4.6 本章小结
输出编码与转义,说白了就是一句话:别让智能体输出的内容,被解释器当成代码执行。
- HTML实体编码:防XSS的基础,但要注意上下文感知
- JSON序列化安全:用标准库,别手写,防原型链污染
- 防止数据泄露:输出过滤器是最后一道防线,脱敏要彻底
嗯,这一章的内容就到这里。记住,输出安全不是可选项,是必选项。你永远不知道智能体会在什么时候,生成一段让你头疼的字符串。