作为拥有三年 AI 应用开发经验的工程师,我深知一个看似无害的 AI 输出可能在网页展示时引发严重的安全问题。去年双十一期间,我负责的智能客服系统在接入某大模型 API 后,遭遇了用户反馈的脚本注入攻击——攻击者通过巧妙的 prompt 注入,让 AI 返回的文本中包含了可执行的 JavaScript 代码,导致部分用户浏览器执行了恶意脚本。这件事让我彻底认识到:AI 输出≠安全文本,必须经过严格的内容清洗才能展示。
为什么 AI 输出中的 XSS 更危险
传统 Web 应用的 XSS 防护我们已经很熟悉了——用户输入 → 服务端过滤 → 页面展示。但 AI 输出场景有三个独特的风险点:
- 输出内容不可预测:AI 模型会生成各种格式的文本,包括 Markdown、代码片段、HTML 标签等,这让传统的白名单过滤变得困难
- Prompt 注入攻击:恶意用户可以通过精心设计的输入,让 AI "听话地"输出包含恶意脚本的内容
- 流式输出场景:现代 AI 应用多采用 SSE/流式传输,边输出边展示的机制更容易绕过一次性过滤
在我测试 HolySheep API 时(该平台提供国内直连<50ms的延迟),我发现其控制台内置了输出预览的安全提示功能,这让我对 AI 输出安全有了更系统的认知。接下来,我会用真实代码演示如何在不同场景下处理 AI 输出的 XSS 风险。
方案一:HTML 实体转义(基础但必要)
这是最简单也是最必须的防护手段。我在使用 HolySheep AI 的 Node.js SDK 时,会在前端展示前统一做一次转义处理:
// 前端工具函数:HTML 实体转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 模拟从 HolyShehe API 获取 AI 响应
async function fetchAIResponse(prompt) {
// HolySheep API 集成示例
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_HOLYSHEEP_API_KEY'
},
body: JSON.stringify({
model: 'gpt-4.1',
messages: [{ role: 'user', content: prompt }],
stream: false
})
});
const data = await response.json();
return data.choices[0].message.content;
}
// 安全展示 AI 输出
async function displayAIResponse(prompt) {
const rawResponse = await fetchAIResponse(prompt);
// ❌ 危险:直接 innerHTML 赋值
// document.getElementById('output').innerHTML = rawResponse;
// ✅ 安全:先转义再赋值
document.getElementById('output').textContent = rawResponse;
// 或者使用转义函数
document.getElementById('output').innerHTML = escapeHtml(rawResponse);
}
方案二:DOMPurify 深度清洗(推荐生产使用)
对于需要支持 Markdown 渲染或富文本展示的场景,我强烈推荐使用 DOMPurify。这个库在我的多个生产项目中表现稳定,配合 HolySheep API 使用时,我通常这样配置:
// 安装:npm install dompurify marked
import DOMPurify from 'dompurify';
import { marked } from 'marked';
// 配置 DOMPurify 允许安全的 Markdown 标签
DOMPurify.setConfig({
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'a', 'span'],
ALLOWED_ATTR: ['href', 'class', 'target'],
ADD_ATTR: ['target'],
FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input', 'img', 'svg', 'object', 'embed'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur']
});
// 安全渲染 Markdown 格式的 AI 输出
function safeRenderMarkdown(aiMarkdownResponse) {
// 1. 将 Markdown 转为 HTML
const rawHtml = marked.parse(aiMarkdownResponse);
// 2. 深度清洗潜在 XSS
const cleanHtml = DOMPurify.sanitize(rawHtml);
// 3. 安全插入 DOM
const container = document.getElementById('ai-content');
container.innerHTML = cleanHtml;
// 4. 强制新窗口打开外部链接
container.querySelectorAll('a[href]').forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
});
return container.innerHTML;
}
// HolySheep API 调用示例(支持流式输出)
async function streamAIResponse(userInput) {
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_HOLYSHEEP_API_KEY'
},
body: JSON.stringify({
model: 'claude-sonnet-4.5',
messages: [{ role: 'user', content: userInput }],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// 解析 SSE 数据(简化版)
if (chunk.startsWith('data: ')) {
const jsonStr = chunk.slice(6);
if (jsonStr === '[DONE]') break;
try {
const data = JSON.parse(jsonStr);
const delta = data.choices[0].delta.content || '';
fullContent += delta;
// ⚠️ 关键:流式输出时也要实时清洗
document.getElementById('streaming-output').innerHTML =
DOMPurify.sanitize(marked.parse(fullContent));
} catch (e) {
console.error('SSE 解析错误:', e);
}
}
}
return fullContent;
}
方案三:后端统一过滤层(企业级方案)
在我负责的企业级 AI 应用中,我采用了后端统一过滤的架构。HolySheep API 的价格优势(GPT-4.1 仅 $8/MTok,Claude Sonnet 4.5 $15/MTok)让我们有更多预算投入到安全层建设。以下是我设计的中间件方案:
# Python 后端:AI 输出安全过滤中间件
pip install bleach python-json-logger
import bleach
from bleach.css_sanitizer import CSSSanitizer
from typing import Callable
from functools import wraps
import logging
logger = logging.getLogger(__name__)
允许的 HTML 标签和属性
ALLOWED_TAGS = [
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's',
'code', 'pre', 'blockquote',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li',
'a', 'span', 'div',
'table', 'thead', 'tbody', 'tr', 'th', 'td'
]
ALLOWED_ATTRIBUTES = {
'*': ['class', 'id'],
'a': ['href', 'title', 'target', 'rel'],
'code': ['class'],
'pre': ['class'],
}
危险的属性和协议
STRIPPED_ATTRS = ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit']
FORBIDDEN_PROTOCOLS = ['javascript:', 'data:', 'vbscript:']
class AIOutputSanitizer:
"""AI 输出安全过滤器"""
def __init__(self):
self.css_sanitizer = CSSSanitizer()
self.stats = {'total': 0, 'cleaned': 0, 'blocked': 0}
def sanitize(self, text: str, strip: bool = False) -> str:
"""
清洗 AI 输出文本
Args:
text: 原始 AI 输出
strip: True=完全移除危险内容, False=转义危险内容
"""
self.stats['total'] += 1
if not text:
return ""
# 1. 预处理:移除可能的 prompt 注入
text = self._remove_prompt_injection(text)
# 2. HTML 清洗
if strip:
cleaned = bleach.clean(
text,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
css_sanitizer=self.css_sanitizer,
strip=True
)
else:
cleaned = bleach.clean(
text,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
css_sanitizer=self.css_sanitizer,
strip=False
)
# 3. 检测残留危险模式
danger_patterns = self._detect_danger_patterns(text)
if danger_patterns:
self.stats['blocked'] += 1
logger.warning(f"检测到危险模式: {danger_patterns}", extra={
'ai_sanitizer': True,
'patterns': danger_patterns
})
return "[内容已被安全过滤]"
if cleaned != text:
self.stats['cleaned'] += 1
logger.info(f"清洗了 {len(text) - len(cleaned)} 个字符")
return cleaned
def _remove_prompt_injection(self, text: str) -> str:
"""移除潜在的 prompt 注入攻击"""
# 移除系统提示伪装
patterns_to_remove = [
r'(system|admin|sudo|ignore previous instructions).*?:',
r'<\s*script[^>]*>.*?<\s*/\s*script\s*>',
r'javascript\s*:',
r'on\w+\s*=\s*["\']',
]
for pattern in patterns_to_remove:
text = bleach.clean(text, tags=[], attributes={}, strip=True)
return text
def _detect_danger_patterns(self, text: str) -> list:
"""检测危险模式"""
import re
dangers = []
danger_regexes = [
(r'