企业官网带新闻发布、方案展示,几乎必有富文本内容。富文本是 XSS 攻击最经典的入口:客户在后台粘贴一篇带恶意脚本的"新闻",前台所有访客都会执行这段脚本。本文给出我们在真实项目中验证过的完整防御体系。
一、XSS 的三种形态
- 存储型:恶意代码存进数据库(如新闻正文
<script>),每个访问者中招——富文本场景的头号风险 - 反射型:恶意代码藏在 URL 参数里,服务端原样输出到页面(如搜索关键词回显)
- DOM 型:前端 JS 直接用
innerHTML拼接不可信数据
共同防御原则只有一个:数据与代码分离——所有不可信内容在渲染前必须失去"执行能力"。
二、富文本白名单过滤:服务端存储前做
不要试图用正则"删掉 script 标签"——绕过姿势成百上千(<img onerror=...>、<svg onload=...>、大小写、编码变体)。正确做法是白名单机制:只保留明确允许的标签与属性,其余一律剔除。
import sanitizeHtml from 'sanitize-html';
const clean = sanitizeHtml(dirtyHtml, {
allowedTags: [
'p', 'br', 'strong', 'em', 'u', 's', 'blockquote',
'h2', 'h3', 'h4', 'ul', 'ol', 'li',
'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
'pre', 'code', 'figure', 'figcaption', 'span', 'div'
],
allowedAttributes: {
a: ['href', 'title', 'target', 'rel'],
img: ['src', 'alt', 'width', 'height', 'loading'],
'*': ['class'], // 仅当样式类来自受控编辑器时开放
},
allowedSchemes: ['http', 'https', 'mailto'], // 禁 javascript: 伪协议
allowedSchemesByTag: { img: ['http', 'https'] },
transformTags: {
a: sanitizeHtml.simpleTransform('a', { rel: 'noopener noreferrer' }),
},
});
三个易错点:
- scheme 必须显式限制:
javascript:alert(1)写在 href 里就是经典 XSS - 不要开放 style 属性:内联样式可用于钓鱼伪装,个别值(
expression())在老 IE 有执行风险 - iframe 默认拒绝:确需嵌入视频,仅放行可信域(如
bilibili.com/player)并加sandbox
三、Content-Security-Policy:最后的安全网
即使某处过滤失效,CSP 也能阻止内联脚本执行。Next.js 在 middleware 或 next.config 中配置:
// middleware.ts
const csp = [
"default-src 'self'",
"script-src 'self' 'nonce-N4x1bVc2'", // 禁止内联 script,仅放行带 nonce 的
"style-src 'self' 'unsafe-inline'", // 样式通常需要内联(Tailwind 注入)
"img-src 'self' https://cdn.example.cn data:",
"frame-src https://player.bilibili.com", // 仅允许可信嵌入
"object-src 'none'",
"base-uri 'self'",
].join('; ');
export function middleware(request) {
const res = NextResponse.next();
res.headers.set('Content-Security-Policy', csp);
return res;
}
上线策略:先以 Content-Security-Policy-Report-Only 模式观察两周,收集违反报告修正规则后再正式启用,避免误杀业务脚本。
四、Cookie 安全
res.setHeader('Set-Cookie', [
'session=xxx;',
'HttpOnly', // JS 无法读取,防脚本窃取
'Secure', // 仅 HTTPS 传输
'SameSite=Lax', // 阻断跨站 CSRF 的主要载荷
'Path=/; Max-Age=604800',
].join(' '));
管理后台的会话 Cookie 三件套(HttpOnly / Secure / SameSite)一项都不能少。
五、其他高频泄露点
| 场景 | 风险 | 处理 |
|---|---|---|
| 搜索词回显 | 反射型 XSS | 输出时 HTML 转义(React 默认转义,勿用 dangerouslySetInnerHTML 包用户输入) |
| URL 参数渲染 | DOM XSS | 用 textContent,禁 innerHTML;跳转前校验白名单域 |
| 文件上传 | 上传 .html/.svg 被直接访问执行 | 存储与访问域名分离 + 强制 Content-Type/Content-Disposition |
| 第三方统计脚本 | 供应链风险 | 固定版本号 + SRI 校验 + CSP 限定域 |
六、防御体系分层图
- 入口过滤:富文本存储前白名单清洗(第一道)
- 输出转义:框架默认转义 + 审计所有 dangerouslySetInnerHTML(第二道)
- CSP 兜底:即使前两道失守,内联脚本也无法执行(第三道)
- 监控告警:CSP report-uri 收集攻击尝试,安全事件可追溯
小结
XSS 防御不是"加个过滤函数",而是一套纵深体系。其中白名单过滤 + CSP 是性价比最高的两块基石,建议所有带 UGC 或富文本的官网第一时间落地。