目瞪狗呆,这一大段又是啥?意思就是 MutationObserver 在观测时并非发现一个新元素就立即回调,而是将一个时间片段里出现的所有元素,一起传过来。所以在回调中我们需要进行批量处理。而且,其中的callback会在指定的 DOM 节点(目标节点)发生变化时被调用。在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个 MutationRecord 对象的数组,第二个参数则是这个观察者对象本身。
所以,使用 MutationObserver ,我们可以对页面加载的每个静态脚本文件,进行监控:
// 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 });
<script type="text/javascript" src="https://www.jb51.net/article/xss/a.js"></script>是页面加载一开始就存在的静态脚本(查看页面结构),我们使用 MutationObserver 可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则removeChild()掉,使之无法执行。
使用白名单对 src 进行匹配过滤上面的代码中,我们判断一个js脚本是否是恶意的,用的是这一句:
if (/xss/i.test(node.src)) {}
当然实际当中,注入恶意代码者不会那么傻,把名字改成 XSS 。所以,我们很有必要使用白名单进行过滤和建立一个拦截上报系统。
// 建立白名单 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 拦截静态脚本,除了静态脚本,与之对应的就是动态生成的脚本。
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 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。
虽然不能用,也可以了解一下:
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 树前的捕获它,那就是创建脚本时这个时机。
假设现在有一个动态脚本是这样创建的: