别再乱用innerHTML了这些安全风险你必须知道
innerHTML的坑,你踩过几个?
最近在重构一个老项目的时候,又遇到了innerHTML这个让人又爱又恨的东西。说真的,我挺喜欢用innerHTML的,直接拼接HTML字符串,简单粗暴效率高。但问题是,这玩意儿的安全隐患实在太多了。
之前就因为直接用了用户输入的数据拼接到innerHTML里,结果被注入了一段恶意脚本,差点把数据库都给删了。从那以后我就特别注意这个问题,也尝试了好几种替代方案,今天就跟大家聊聊我的实战经验。
谁更安全?三种方案大PK
其实要解决innerHTML的安全问题,主流有三个方案:DOM方法、模板引擎和sanitize-html库。我个人比较推荐第三个,但具体还是得看场景。
原生DOM操作:安全但麻烦
最原始的方法就是用原生的DOM操作API,比如createElement、appendChild这些:
// 原生DOM写法
const container = document.getElementById('container');
const newDiv = document.createElement('div');
newDiv.textContent = '这是安全的内容';
container.appendChild(newDiv);
这种方法确实很安全,因为所有内容都是通过textContent设置的,不会执行任何HTML代码。但我个人觉得太繁琐了,特别是当你要动态生成复杂结构时,写起来简直要命。
记得有一次我用这种方式重构了一个表单组件,光是append各种元素就写了两百多行代码,维护起来特别痛苦。所以除非是特别简单的场景,不然我不太喜欢用这种写法。
模板引擎:灵活但需要学习成本
第二种是用模板引擎,比如Handlebars或者EJS。这里以Handlebars为例:
<!-- HTML部分 -->
<script id="template" type="text/x-handlebars-template">
<div>{{content}}</div>
</script>
<!-- JavaScript部分 -->
<script src="https://cdn.jsdelivr.net/npm/handlebars/dist/handlebars.min.js"></script>
<script>
const source = document.getElementById('template').innerHTML;
const template = Handlebars.compile(source);
const context = { content: "这是安全的内容" };
document.getElementById('container').innerHTML = template(context);
</script>
模板引擎的好处是既安全又灵活,它会自动对特殊字符进行转义。但说实话,为了这么个功能专门引入一个库,感觉有点杀鸡用牛刀。而且团队里要是有人不熟悉这个模板语法,后续维护可能会遇到麻烦。
我之前在一个项目里用过Handlebars,虽然效果不错,但新来的同事总抱怨看不懂那些{{}}符号。所以现在除非项目本身已经在用模板引擎了,否则我一般不会特意去引入。
sanitize-html:我的最爱
最后就是我要重点推荐的sanitize-html库了:
// 安装:npm install sanitize-html
// 使用示例
const sanitizeHtml = require('sanitize-html');
const dirty = '<img src=x onerror=alert("XSS")>';
const clean = sanitizeHtml(dirty, {
allowedTags: ['b', 'i', 'em', 'strong'],
allowedAttributes: {}
});
document.getElementById('container').innerHTML = clean;
这个库简直是神器,既能防止XSS攻击,又能保留你需要的HTML结构。配置也很灵活,想允许哪些标签和属性都可以自定义。
我在好几个项目里都用了这个方案,特别是在处理富文本编辑器的内容时特别好用。像我们之前对接的一个第三方评论系统,返回的内容全是带格式的HTML,用sanitize-html处理起来就很方便。
不过要注意的是,默认配置可能过于严格,有时候需要根据实际需求调整allowedTags和allowedAttributes。比如上次我就忘记加上a标签的href属性,导致所有的链接都失效了,调试了半天才发现问题出在这。
性能对比:差距比我想象的小
说到性能,很多人可能会觉得原生DOM操作最快,但实际上差别并没有那么大。我特意做过一个简单的测试,在插入1000个节点的情况下:
- 原生DOM操作:约80ms
- sanitize-html:约120ms
- Handlebars:约150ms
可以看到,虽然原生方法确实快一些,但其他方案也没慢到哪里去。考虑到现代浏览器的性能,这点差异在大多数场景下都可以忽略不计。
我的选型逻辑
综合考虑下来,我的选型偏好是这样的:
- 如果只是简单的内容展示,我会选择原生DOM操作,毕竟不需要额外依赖
- 对于需要处理富文本的场景,sanitize-html是我的首选,灵活又安全
- 如果项目本身已经在使用模板引擎了,那就顺手用模板引擎来处理
举个例子,我们现在正在开发的一个博客系统,文章内容需要用富文本编辑器,我就直接上了sanitize-html。而对于一些简单的提示信息,就用原生的textContent来处理。
踩坑提醒:这三点一定注意
最后再给大家提几个我踩过的坑:
- 别忘了配置项:用sanitize-html的时候一定要仔细检查allowedTags和allowedAttributes,否则可能会过滤掉你需要的内容
- 注意编码问题:有些特殊字符在不同环境下表现可能不一样,最好统一使用UTF-8编码
- 测试要充分:特别是涉及到用户输入的地方,一定要做足安全测试,我之前就是因为测试不够全面吃了大亏
以上是我对innerHTML安全问题的一些实践总结,有不同看法欢迎评论区交流。这个话题其实还有很多可以深挖的地方,比如CSP策略的配合使用等,有机会再跟大家分享。

暂无评论