XSS库实战指南:防御与绕过技巧全解析

Newb.庆玲 安全 阅读 2,305
赞 18 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周上线一个用户评论功能,结果 QA 一测就炸了:有人在评论里贴了段 <script>alert(1)</script>,页面直接弹窗。我赶紧翻了下代码,发现后端只做了简单过滤,前端更是完全没处理。这不就是典型的 XSS 漏洞吗?赶紧补救。

XSS库实战指南:防御与绕过技巧全解析

折腾半天,最后用了 xss 这个库,亲测有效。核心代码就几行:

import { clean } from 'xss';

const userInput = '<script>alert("xss")</script><p>正常内容</p>';
const safeOutput = clean(userInput);
console.log(safeOutput); // 输出: &lt;script&gt;alert("xss")&lt;/script&gt;&lt;p&gt;正常内容&lt;/p&gt;

直接把危险标签转义成纯文本,简单粗暴但管用。如果你项目里要处理用户输入的富文本(比如评论、帖子、个人简介),建议直接用这种方式,别自己写正则,容易漏。

这个场景最好用

不是所有地方都需要全量过滤。比如我们有个后台管理系统,管理员可以编辑一段 HTML 介绍,但只允许用 <p><strong><em><a> 这几个标签。这时候就得自定义白名单了。

我一开始图省事,直接用默认配置,结果用户贴了个带样式的 <div style="position:fixed;top:0">,整个页面布局被搞乱了。后来研究了下文档,搞了个白名单配置:

import { clean, whiteList } from 'xss';

// 先复制默认白名单
const myWhiteList = { ...whiteList };

// 只保留我们需要的标签
myWhiteList.p = [];
myWhiteList.strong = [];
myWhiteList.em = [];
myWhiteList.a = ['href', 'target']; // a 标签只允许 href 和 target 属性

const options = {
  whiteList: myWhiteList,
  stripIgnoreTag: true, // 过滤不在白名单的标签(而不是转义)
  onIgnoreTagAttr: (tag, name, value, isWhiteAttr) => {
    // 如果是 a 标签的 href,做额外校验
    if (tag === 'a' && name === 'href') {
      // 简单校验是否为 http/https 开头
      if (!value.startsWith('http://') && !value.startsWith('https://')) {
        return ''; // 返回空字符串,即删除该属性
      }
    }
  }
};

const dirty = '<p>欢迎访问 <a href="javascript:alert(1)">恶意链接</a></p><script>alert(2)</script>';
const cleanHtml = clean(dirty, options);
console.log(cleanHtml); // 输出: <p>欢迎访问 <a>恶意链接</a></p>

这里注意:stripIgnoreTag: true 很关键。默认行为是把非法标签转义成 <script>,但如果你只想保留白名单内容,应该直接删掉非法标签,不然页面会显示一堆乱码字符。

踩坑提醒:这三点一定注意

我踩过好几次坑,总结下来这三个点最容易出问题:

  • 别信“看起来安全”的输入:用户可能贴一段看似无害的 HTML,比如 <img src=x onerror=alert(1)>。默认配置下,xss 会保留 img 标签,但会过滤掉 onerror 属性,所以其实安全。但如果你自定义了白名单,又忘了处理事件属性,就可能中招。建议:除非你明确知道需要哪些属性,否则不要随便加标签到白名单。
  • URL 校验别偷懒:上面例子中我对 href 做了简单校验,但实际项目中可能更复杂。比如要支持相对路径、mailto、tel 等。如果只是内部系统,限制死 https 就行;如果是公开网站,得用更严谨的 URL 解析库(比如 new URL())来判断,避免 javascript:vbscript:data: 等协议。
  • 别在服务端和前端都过滤:我见过有团队在 Node.js 服务端用 xss 清理一遍,前端又清理一遍。结果双重转义,页面显示 &lt;p&gt;内容&lt;/p&gt;。正确做法是:数据入库前清理一次(或存原始数据 + 单独存清理后数据),前端展示时直接用清理后的数据。如果前后端都用 JS,可以共享同一套配置,但别重复执行。

高级技巧:动态配置与性能

项目大了之后,不同模块的富文本规则可能不一样。比如文章正文允许 imgvideo,但用户签名只允许纯文本。这时候可以把配置抽成函数:

// xssConfig.js
import { whiteList } from 'xss';

export const getProfileXssOptions = () => ({
  whiteList: { b: [], i: [], em: [] }, // 仅允许基础格式
  stripIgnoreTag: true
});

export const getPostXssOptions = () => {
  const wl = { ...whiteList };
  // 默认白名单已经包含 img、a 等,但我们要限制 img 的 src
  wl.img = ['src', 'alt'];
  return {
    whiteList: wl,
    stripIgnoreTag: true,
    onTagAttr: (tag, name, value) => {
      if (tag === 'img' && name === 'src') {
        // 只允许来自可信域名的图片
        if (!value.startsWith('https://jztheme.com/uploads/')) {
          return 'src=""'; // 替换为无效 src
        }
      }
    }
  };
};

然后在业务代码里按需调用:

import { clean } from 'xss';
import { getProfileXssOptions, getPostXssOptions } from './xssConfig';

// 用户资料页
const safeBio = clean(user.bio, getProfileXssOptions());

// 文章详情页
const safeContent = clean(post.content, getPostXssOptions());

关于性能:实测在普通内容(1KB 以内)上,clean() 耗时基本在 0.1ms 以下,对用户体验无感。但如果要处理超长内容(比如整篇小说),建议放到 Web Worker 里,或者至少做防抖——别在用户每敲一个字就跑一次清理。

最后说两句

XSS 防御没有银弹,xss 库只是其中一环。最稳妥的做法还是:输入验证 + 输出编码 + CSP(内容安全策略)三管齐下。不过对于大多数中小型项目,用好这个库已经能挡住 95% 的攻击了。

以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如结合 Markdown 解析器、自定义过滤规则等),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论