别再乱用innerHTML了这些安全风险你必须知道

博主爱巧 安全 阅读 1,230
赞 22 收藏
二维码
手机扫码查看
反馈

innerHTML的坑,你踩过几个?

最近在重构一个老项目的时候,又遇到了innerHTML这个让人又爱又恨的东西。说真的,我挺喜欢用innerHTML的,直接拼接HTML字符串,简单粗暴效率高。但问题是,这玩意儿的安全隐患实在太多了。

别再乱用innerHTML了这些安全风险你必须知道

之前就因为直接用了用户输入的数据拼接到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

可以看到,虽然原生方法确实快一些,但其他方案也没慢到哪里去。考虑到现代浏览器的性能,这点差异在大多数场景下都可以忽略不计。

我的选型逻辑

综合考虑下来,我的选型偏好是这样的:

  1. 如果只是简单的内容展示,我会选择原生DOM操作,毕竟不需要额外依赖
  2. 对于需要处理富文本的场景,sanitize-html是我的首选,灵活又安全
  3. 如果项目本身已经在使用模板引擎了,那就顺手用模板引擎来处理

举个例子,我们现在正在开发的一个博客系统,文章内容需要用富文本编辑器,我就直接上了sanitize-html。而对于一些简单的提示信息,就用原生的textContent来处理。

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

最后再给大家提几个我踩过的坑:

  • 别忘了配置项:用sanitize-html的时候一定要仔细检查allowedTags和allowedAttributes,否则可能会过滤掉你需要的内容
  • 注意编码问题:有些特殊字符在不同环境下表现可能不一样,最好统一使用UTF-8编码
  • 测试要充分:特别是涉及到用户输入的地方,一定要做足安全测试,我之前就是因为测试不够全面吃了大亏

以上是我对innerHTML安全问题的一些实践总结,有不同看法欢迎评论区交流。这个话题其实还有很多可以深挖的地方,比如CSP策略的配合使用等,有机会再跟大家分享。

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

暂无评论