JavaScript防http劫持与XSS
http://www.cnblogs.com/coco1s/p/5777260.html
作为前端,一直以来都知道HTTP劫持
与XSS跨站脚本
(Cross-site scripting)、CSRF跨站请求伪造
(Cross-site request forgery)。但是一直都没有深入研究过,前些日子同事的分享会偶然提及,我也对这一块很感兴趣,便深入研究了一番。
除去一些未列出来的非常少见生僻的注入方式,大部分都是 javascript:...
及内联事件 on*
。
我们假设注入已经发生,那么有没有办法拦截这些内联事件与内联脚本的执行呢?
对于上面列出的 (1) (5) ,这种需要用户点击或者执行某种事件之后才执行的脚本,我们是有办法进行防御的。
的 a 标签而言,真正触发元素 alert(222)
是处于点击事件的目标阶段。
点击上面的 click me
,先弹出 111 ,后弹出 222。
那么,我们只需要在点击事件模型的捕获阶段对标签内 javascript:...
的内容建立关键字黑名单,进行过滤审查,就可以做到我们想要的拦截效果。
对于 on* 类内联事件也是同理,只是对于这类事件太多,我们没办法手动枚举,可以利用代码自动枚举,完成对内联事件及内联脚本的拦截。
以拦截 a 标签内的 href="javascript:...
为例,我们可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // 建立关键词黑名单 var keywordBlackList = [ 'xss' , 'BAIDU_SSP__wrapper' , 'BAIDU_DSPUI_FLOWBAR' ]; document.addEventListener( 'click' , function (e) { var code = "" ; // 扫描 的脚本 if (elem.tagName == 'A' && elem.protocol == 'javascript:' ) { var code = elem.href.substr(11); if (blackListMatch(keywordBlackList, code)) { // 注销代码 elem.href = 'javascript:void(0)' ; console.log( '拦截可疑事件:' + code); } } }, true ); /** * [黑名单匹配] * @param {[Array]} blackList [黑名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function blackListMatch(blackList, value) { var length = blackList.length, i = 0; for (; i < length; i++) { // 建立黑名单正则 var reg = new RegExp(whiteList[i], 'i' ); // 存在黑名单中,拦截 if (reg.test(value)) { return true ; } } return false ; } |
可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)
点击图中这几个按钮,可以看到如下:
这里我们用到了黑名单匹配,下文还会细说。
静态脚本拦截
XSS 跨站脚本的精髓不在于“跨站”,在于“脚本”。
通常而言,攻击者或者运营商会向页面中注入一个脚本,具体操作都在脚本中实现,这种劫持方式只需要注入一次,有改动的话不需要每次都重新注入。
我们假定现在页面上被注入了一个 脚本,我们的目标就是拦截这个脚本的执行。
听起来很困难啊,什么意思呢。就是在脚本执行前发现这个可疑脚本,并且销毁它使之不能执行内部代码。
所以我们需要用到一些高级 API ,能够在页面加载时对生成的节点进行检测。
MutationObserver
MutationObserver 是 HTML5 新增的 API,功能很强大,给开发者们提供了一种能在某个范围内的 DOM 树发生变化时作出适当反应的能力。
说的很玄乎,大概的意思就是能够监测到页面 DOM 树的变换,并作出反应。
MutationObserver()
该构造函数用来实例化一个新的Mutation观察者对象。
1 2 3 | MutationObserver( function callback ); |
目瞪狗呆,这一大段又是啥?意思就是 MutationObserver 在观测时并非发现一个新元素就立即回调,而是将一个时间片段里出现的所有元素,一起传过来。所以在回调中我们需要进行批量处理。而且,其中的 callback
会在指定的 DOM 节点(目标节点)发生变化时被调用。在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个 MutationRecord 对象的数组,第二个参数则是这个观察者对象本身。
所以,使用 MutationObserver ,我们可以对页面加载的每个静态脚本文件,进行监控:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // MutationObserver 的不同兼容性写法 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; // 该构造函数用来实例化一个新的 Mutation 观察者对象 // Mutation 观察者对象能监听在某个范围内的 DOM 树变化 var observer = new MutationObserver( function (mutations) { mutations.forEach( function (mutation) { // 返回被添加的节点,或者为null. var nodes = mutation.addedNodes; for ( var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (/xss/i.test(node.src))) { try { node.parentNode.removeChild(node); console.log( '拦截可疑静态脚本:' , node.src); } catch (e) {} } } }); }); // 传入目标节点和观察选项 // 如果 target 为 document 或者 document.documentElement // 则当前文档中所有的节点添加与删除操作都会被观察到 observer.observe(document, { subtree: true , childList: true }); |
可以看到如下:可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)
是页面加载一开始就存在的静态脚本(查看页面结构),我们使用 MutationObserver 可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则
removeChild()
掉,使之无法执行。
使用白名单对 src 进行匹配过滤
上面的代码中,我们判断一个js脚本是否是恶意的,用的是这一句:
1 | if (/xss/i.test(node.src)) {} |
当然实际当中,注入恶意代码者不会那么傻,把名字改成 XSS 。所以,我们很有必要使用白名单进行过滤和建立一个拦截上报系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // 建立白名单 var whiteList = [ 'www.aaa.com' , 'res.bbb.com' ]; /** * [白名单匹配] * @param {[Array]} whileList [白名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function whileListMatch(whileList, value) { var length = whileList.length, i = 0; for (; i < length; i++) { // 建立白名单正则 var reg = new RegExp(whiteList[i], 'i' ); // 存在白名单中,放行 if (reg.test(value)) { return true ; } } return false ; } // 只放行白名单 if (!whileListMatch(blackList, node.src)) { node.parentNode.removeChild(node); } |
这里我们已经多次提到白名单匹配了,下文还会用到,所以可以这里把它简单封装成一个方法调用。
动态脚本拦截
上面使用 MutationObserver 拦截静态脚本,除了静态脚本,与之对应的就是动态生成的脚本。
1 2 3 4 5 | var script = document.createElement( 'script' ); script.type = 'text/javascript' ; script.src = 'http://www.example.com/xss/b.js' ; document.getElementsByTagName( 'body' )[0].appendChild(script); |
要拦截这类动态生成的脚本,且拦截时机要在它插入 DOM 树中,执行之前,本来是可以监听 Mutation Events
中的 DOMNodeInserted
事件的。
Mutation Events 与 DOMNodeInserted
打开 MDN ,第一句就是:
该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。
虽然不能用,也可以了解一下:
1 2 3 4 5 6 7 | document.addEventListener( 'DOMNodeInserted' , function (e) { var node = e.target; if (/xss/i.test(node.src) || /xss/i.test(node.innerHTML)) { node.parentNode.removeChild(node); console.log( '拦截可疑动态脚本:' , node); } }, true ); |
然而可惜的是,使用上面的代码拦截动态生成的脚本,可以拦截到,但是代码也执行了:DOMNodeInserted
顾名思义,可以监听某个 DOM 范围内的结构变化,与 MutationObserver
相比,它的执行时机更早。
但是 DOMNodeInserted
不再建议使用,所以监听动态脚本的任务也要交给 MutationObserver
。
可惜的是,在实际实践过程中,使用 MutationObserver
的结果和 DOMNodeInserted
一样,可以监听拦截到动态脚本的生成,但是无法在脚本执行之前,使用 removeChild
将其移除,所以我们还需要想想其他办法。
重写 setAttribute 与 document.write
重写原生 Element.prototype.setAttribute 方法
在动态脚本插入执行前,监听 DOM 树的变化拦截它行不通,脚本仍然会执行。
那么我们需要向上寻找,在脚本插入 DOM 树前的捕获它,那就是创建脚本时这个时机。
假设现在有一个动态脚本是这样创建的:
1 2 3 4 5 | var script = document.createElement( 'script' ); script.setAttribute( 'type' , 'text/javascript' ); script.setAttribute( 'src' , 'http://www.example.com/xss/c.js' ); document.getElementsByTagName( 'body' )[0].appendChild(script); |
而重写 Element.prototype.setAttribute
也是可行的:我们发现这里用到了 setAttribute 方法,如果我们能够改写这个原生方法,监听设置 src
属性时的值,通过黑名单或者白名单判断它,就可以判断该标签的合法性了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // 保存原有接口 var old_setAttribute = Element.prototype.setAttribute; // 重写 setAttribute 接口 Element.prototype.setAttribute = function (name, value) { // 匹配到
|