XSS库实战指南:防御与绕过技巧全解析
先看效果,再看代码
上周上线一个用户评论功能,结果 QA 一测就炸了:有人在评论里贴了段 <script>alert(1)</script>,页面直接弹窗。我赶紧翻了下代码,发现后端只做了简单过滤,前端更是完全没处理。这不就是典型的 XSS 漏洞吗?赶紧补救。
折腾半天,最后用了 xss 这个库,亲测有效。核心代码就几行:
import { clean } from 'xss';
const userInput = '<script>alert("xss")</script><p>正常内容</p>';
const safeOutput = clean(userInput);
console.log(safeOutput); // 输出: <script>alert("xss")</script><p>正常内容</p>
直接把危险标签转义成纯文本,简单粗暴但管用。如果你项目里要处理用户输入的富文本(比如评论、帖子、个人简介),建议直接用这种方式,别自己写正则,容易漏。
这个场景最好用
不是所有地方都需要全量过滤。比如我们有个后台管理系统,管理员可以编辑一段 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清理一遍,前端又清理一遍。结果双重转义,页面显示<p>内容</p>。正确做法是:数据入库前清理一次(或存原始数据 + 单独存清理后数据),前端展示时直接用清理后的数据。如果前后端都用 JS,可以共享同一套配置,但别重复执行。
高级技巧:动态配置与性能
项目大了之后,不同模块的富文本规则可能不一样。比如文章正文允许 img、video,但用户签名只允许纯文本。这时候可以把配置抽成函数:
// 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 解析器、自定义过滤规则等),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论