阅读 404

前端安全: DOM-XSS 漏洞

起因

前段时间在逛某个网站时发现有个页面样式错乱,出于职业习惯,打开控制台看了下,发现 div 上莫名的多了个width:200px的样式,顺着这个线索,经过调试,最终发现了页面上的一个安全漏洞.

为了更好的说明问题和解法方案,我写一个 Demo,可以在 github.com/XYShaoKang/… 中查看代码,也可以直接在 xyshaokang.github.io/dom-xss-dem… 实际去测试文中的例子

整个页面分为上下两大区域,上面是使用不安全的方式插入文本,下面使用安全的方式插入文本.每个区域可以在左边输入文本,点击插入之后,在右边查看效果和源码.

xss-demo.png

找到问题

通过分析是因为页面中有部分内容是通过 innerHTML 动态设置的,正好其中某个帖子的内容中包含了<style> div{width:200px;...,虽然只有一部分内容,但现代浏览器太聪明了,就算只有 Start tags,而没有 End tags 时会帮忙补上,最终实际会在 DOM 中的创建一个 style 元素,而其中的样式div{width:200px;...也能正确的解析出来,导致页面中所有 div 元素的宽度变成 200px.

innerhtml-style.gif

最终结果

innerhtml-style.png

这里只有 CSS 代码,影响的只是这个页面上的样式而已,但如果被插入的是可执行的 JavsScript 代码呢,比如用 innerHTML 去设置下面 content 这段字符串,就会弹出一个显示error的提示框

<div id="app"></div> <script>   const content = `<img onerror="void function(){     alert('error')   }()" src="data:image/png,">`   document.querySelector('#app').innerHTML = content </script> 复制代码

error.gif

如果把其中的alert('error')换成其他的代码,比如获取当前账户的某些敏感信息,或者把这些信息提交给某个 url.更厉害的是如果content中的数据是用户提交的话,要做出什么事来,就全靠用户的想象力了.

不过如果是直接使用 innerHTML 插入 script 标签,却反而不会执行.根据 HTML5 标准规定,通过 innerHTML 插入的 script 元素,在插入时不应该执行.

html5-innerhtml0.png

如何解决

那既然已经知道了问题,要怎么去解决呢,总不能不显示用户输入吧,或者不显示那些有问题的输入(这也可以解决问题,只是不够优雅)?

其实解决起来也很简单,根本原因是因为使用 innerHTML 插入内容时:

  1. 会去解析内容

  2. 生成对应的 DOM

只要能够阻断这两个步骤中的任意一步,都能解决问题

方法 1: 让字符串无法被解析成 DOM

第一种方法,想办法让字符串变成无法被解析成 DOM,在将字符串解析成 DOM 的过程中,最关键是找到 Start tags,也就是开始标签,而解析开始标签的关键是必须要找到<,所以其实只要没有这个小于号,那所有字符都会被解析为文本节点,只要将<替换为空,就可以达到目的,不会生成 DOM,也就不会去执行其中的代码.

不过这样会造成信息上的缺失,用户输入的内容中明明有小于号,硬生生给人家变没就不太好了,其实 HTML 中一类特殊的存在 - 字符实体(Entity),可以通过实体来编写某些想要显示但又不想被解析的字符,比如 HTML 源码中的&lt;会被显示成<,&gt;会被显示成>,可以使用如下方式(虽然只要替换<就能阻止被解析成 DOM,不过>也是 HTML 中的特殊字符,顺便也替换掉):

document.querySelector('#root').innerHTML = content.replace(/</g, '&lt;').replace(/</g, '&gt;') // <div>123456789</div> -> &lt;div&gt;123456789&lt;/div&gt; 复制代码

safe.png

方法 2: 不去解析字符串

第二种方法(推荐),则是直接不去解析字符串,不用 innerHTML,而是使用更安全并且效率更高的 textContent,使用 textContent 设置时,不会把字符串解析为 HTML.

document.querySelector('#root').textContent = content 复制代码

需要生成 DOM 的方案

上面两种方法对于动态设置字符串是比较有效的,如果是打算使用字符串来生成 DOM 的话就不太合适了,可以考虑使用 DOMPurify 或者 sanitize-html,关于两者的更多信息可以查看对应的 GitHub 查看

import sanitizeHtml from 'sanitize-html' import DOMPurify from 'dompurify' const source = `<div><img onerror="alert('error')" src="data:image/png,"/></div>` console.log(sanitizeHtml(source)) // <div></div> console.log(DOMPurify.sanitize(source)) // <div><img src="data:image/png,"></div> 复制代码

原生方案

原生方案中,有两个相关的实验性 API: Sanitizer 和 Element.setHTML(),目前兼容性还有点惨,要用的话,可能还要等挺长的时间.

后记

这篇文章只是讨论 innerHTML 的危险性,而其它类似的像document.write,jQuery 中的 html 方法,React 中的 dangerouslySetInnerHTML,Vue 中的 v-html 等等. 每个框架都会有这种可能存在有风险的 API,这里只是列了几个出来,没有列出来的还有很多,光靠人脑去记是比较不靠谱的,可以考虑根据所使用的框架,将对应有风险的 API 加到团队的规范里面,另外可以在 ESLint 中添加对应的警告,至少让用的人了解其中潜在的风险.

实际上从用户提交数据,到最终渲染到浏览器中,中间会进过很多地方,都有机会进行防御,比如在用户提交到数据库的时候就进行过滤,或者在后端接口中过滤好之后返回给前端也都是可以解决问题,那为什么我们前端还需要去这么折腾呢?我的理解是,我们不能去假设后端一定会过滤,或者一定能正确过滤,我们前端守好最后一道防线,这样即使后端因为某些原因返回的是没有过滤的数据(比如新增了一个接口),依然能让页面正常运转,并且杜绝风险.


作者:XYShaoKang
链接:https://juejin.cn/post/7027884930663186462


文章分类
前端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐