<script type="text/javascript" src="./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 树前的捕获它,那就是创建脚本时这个时机。
假设现在有一个动态脚本是这样创建的:
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 属性时的值,通过黑名单或者白名单判断它,就可以判断该标签的合法性了。
// 保存原有接口 var old_setAttribute = Element.prototype.setAttribute; // 重写 setAttribute 接口 Element.prototype.setAttribute = function(name, value) { // 匹配到 <script src='xxx' > 类型 if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) { // 白名单匹配 if (!whileListMatch(whiteList, value)) { console.log('拦截可疑模块:', value); return; } } // 调用原始接口 old_setAttribute.apply(this, arguments); }; // 建立白名单 var whiteList = [ 'www.yy.com', 'res.cont.yy.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; }
可以看到如下结果:可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)