一、为什么富文本里的XSS还反复出现?

很多做过前端项目的开发者都遇到过这种情况:明明给富文本内容做了过滤,还是会出现恶意代码,甚至有用户反馈账号被偷、页面被跳转。这种存储型XSS(恶意代码被存到数据库,其他用户打开就中招)屡禁不止,核心大多是白名单和事件属性过滤这两个环节出了漏洞。

1.1 最常见的“漏判”:白名单没卡到根上

白名单就是我们提前说好“只允许XX内容进来,其他都不行”,但很多人的白名单写得太粗了,只盯着标签,没锁死标签里的属性。举个实际的代码例子,用前端JavaScript处理富文本:

// 技术栈:JavaScript(前端DOM内容过滤)
// 【漏洞版】只过滤标签,不限制属性
function badSaveRichText(content) {
  // 白名单只允许这些标签,常见的文本标签
  const allowedTags = ['p', 'span', 'div', 'a'];
  const tempDiv = document.createElement('div');
  tempDiv.innerHTML = content;

  // 遍历所有节点,删掉不在白名单里的标签
  Array.from(tempDiv.childNodes).forEach(node => {
    if (node.nodeType === Node.ELEMENT_NODE && !allowedTags.includes(node.tagName.toLowerCase())) {
      node.remove();
    }
  });
  // 错误:完全没检查标签的属性,比如a标签的href
  return tempDiv.innerHTML;
}

// 测试用例:用户输入带恶意链接的内容
const maliciousContent = '<a href="javascript:alert(1)">点我拿福利</a>';
// 用漏洞函数保存后,内容会被存进数据库,用户点击就会弹出恶意提示
badSaveRichText(maliciousContent);

这个例子里,a标签本身在白名单里,所以会被保留,但href属性用了javascript:协议,点击就会执行任意代码,这就是典型的白名单边界没踩准的问题。

1.2 另一个坑:只盯事件属性,放过了“隐形代码”

有的开发者怕事件属性(比如onclick、onmouseover)被利用,所以专门过滤这些,但还是会漏:比如用户把事件名编码,比如把onclick写成&#111;nclick,过滤工具只找明文的onxxx,就会漏掉,渲染后浏览器会自动解码,还是会触发XSS。

二、白名单策略的常见失效场景

2.1 标签允许,但属性没锁死

刚才的a标签例子就是典型,类似的还有div标签的style属性,用户可以在style里加expression(alert(1)),这个表达式会被浏览器解析执行,即使是div标签在白名单里,也会被漏洞利用。修复这个问题很简单,把属性也加入白名单,只允许安全的属性:

// 【修复版】同时过滤标签和属性
function safeSaveRichText(content) {
  // 缩小白名单标签,去掉可能有风险的a标签
  const allowedTags = ['p', 'span', 'div'];
  const tempDiv = document.createElement('div');
  tempDiv.innerHTML = content;

  Array.from(tempDiv.childNodes).forEach(node => {
    if (node.nodeType === Node.ELEMENT_NODE) {
      // 先过滤标签
      if (!allowedTags.includes(node.tagName.toLowerCase())) {
        node.remove();
        return;
      }
      // 再过滤属性:只允许class、style,拒绝href、onxxx等
      const allowedAttrs = ['class', 'style'];
      Array.from(node.attributes).forEach(attr => {
        if (!allowedAttrs.includes(attr.name.toLowerCase())) {
          node.removeAttribute(attr.name);
        }
        // 额外过滤style里的危险表达式
        if (attr.name.toLowerCase() === 'style' && attr.value.includes('expression')) {
          node.removeAttribute('style');
        }
      });
    }
  });
  return tempDiv.innerHTML;
}

2.2 忽略了非标准标签的风险

很多富文本编辑器会允许用户插入svg、mathml这类标签,用来做图形或者数学公式,但这些标签的事件属性特别多,比如onloadonshow,之前的白名单如果没把这些标签的事件过滤,就会被攻击者利用,比如<svg onload=alert(document.domain)>,这个内容会被保留,打开页面就执行恶意代码。

三、事件属性过滤的盲区

3.1 编码绕过的常见方式

除了刚才说的实体编码,还有Unicode编码,比如把onload写成\u006f\u006e\u006c\u006f\u0061\u0064,过滤工具如果没识别这种编码,就会漏掉,等渲染后会变成明文的onload事件,触发XSS。

3.2 容易遗漏的事件类型

除了常见的onclick、onmouseover,还有很多不显眼的事件,比如ontoggle(展开详情时触发)、onscroll(滚动时触发)、onresize(元素大小变化时触发),这些事件大多过滤器都会忽略,攻击者只要把这些事件加在普通标签里,比如<div ontoggle=alert(1)>,只要用户触发对应操作,就会中招。

四、怎么快速定位和修复这类漏洞

4.1 根因定位的小技巧

不用学复杂的安全工具,用最笨的方法就行:构造不同的XSS payload,一个个试。比如先试最简单的<img src=x onerror=alert(1)>,再试<svg onload=alert(1)>,再试编码后的事件&#111;nload,看哪个payload能绕过过滤,找到漏洞点。另外,用Chrome的Elements面板,把恶意内容粘贴进去,看哪些元素被保留了,哪些属性被过滤了,就能快速找到漏的地方。

4.2 修复时的关键注意事项

第一,一定要用白名单策略,绝对不能用黑名单,因为黑名单永远有漏,比如你拉黑了onclick,还有onmouseover、onload、ontoggle等几十种事件,根本拦不完;第二,多层防护,既要过滤标签,又要过滤属性,还要过滤属性里的危险协议(比如href不能用javascript:,src不能用data:);第三,定期更新过滤规则,因为浏览器的HTML标准一直在变,新的标签和属性可能会出现,比如最近的web组件标签,可能会带来新的风险。

五、实际项目中的修复案例

之前我参与过一个社区论坛项目,用的是开源富文本编辑器,有用户反馈账号被偷。排查后发现两个问题:一是白名单允许a标签,但没限制href,用户可以插入javascript:alert(document.cookie)这样的链接,点击就能拿到其他用户的cookie;二是允许div标签,div的style属性里可以加expression(stealCookie()),执行后把cookie发到攻击者的服务器。修复的时候,我们把a标签从白名单里删掉,把div的style属性过滤掉所有危险内容,同时把svg标签加入白名单,并且过滤它的所有事件属性,之后测试了上百种payload,都没再发现XSS漏洞,用户反馈也恢复正常了。

六、最后再唠唠心得

很多开发者觉得富文本XSS是小事,反正大部分用户不会输入恶意内容,但其实攻击者是专门找这种偷懒的地方。存储型XSS的危害比反射型大得多,因为它被存在数据库里,所有访问这个内容的用户都会中招,比如盗取账号、跳转恶意网站、下载病毒。所以防护的时候一定要严格,不能嫌麻烦,比如不要只做一层过滤,要多层兜底,用白名单,不要用黑名单,还要定期测试,确保过滤规则有效。另外,做项目的时候,不要随便用开源的富文本编辑器,一定要自己根据业务需求修改过滤规则,开源的编辑器大多是通用的,没针对特定业务做安全优化。