企业官网带新闻发布、方案展示,几乎必有富文本内容。富文本是 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 限定域

六、防御体系分层图

  1. 入口过滤:富文本存储前白名单清洗(第一道)
  2. 输出转义:框架默认转义 + 审计所有 dangerouslySetInnerHTML(第二道)
  3. CSP 兜底:即使前两道失守,内联脚本也无法执行(第三道)
  4. 监控告警:CSP report-uri 收集攻击尝试,安全事件可追溯

小结

XSS 防御不是"加个过滤函数",而是一套纵深体系。其中白名单过滤 + CSP 是性价比最高的两块基石,建议所有带 UGC 或富文本的官网第一时间落地。