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='https://www.jb51.net/article/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; }
重写Element.prototype.setAttribute,就是首先保存原有接口,然后当有元素调用 setAttribute 时,检查传入的 src 是否存在于白名单中,存在则放行,不存在则视为可疑元素,进行上报并不予以执行。最后对放行的元素执行原生的setAttribute,也就是old_setAttribute.apply(this, arguments);。
上述的白名单匹配也可以换成黑名单匹配。
重写嵌套 iframe 内的 Element.prototype.setAttribute当然,上面的写法如果old_setAttribute = Element.prototype.setAttribute暴露给攻击者的话,直接使用old_setAttribute就可以绕过我们重写的方法了,所以这段代码必须包在一个闭包内。
当然这样也不保险,虽然当前窗口下的Element.prototype.setAttribute已经被重写了。但是还是有手段可以拿到原生的Element.prototype.setAttribute,只需要一个新的 iframe 。
var newIframe = document.createElement('iframe'); document.body.appendChild(newIframe); Element.prototype.setAttribute = newIframe.contentWindow.Element.prototype.setAttribute;
通过这个方法,可以重新拿到原生的Element.prototype.setAttribute,因为 iframe 内的环境和外层 window 是完全隔离的。wtf?
怎么办?我们看到创建 iframe 用到了createElement,那么是否可以重写原生createElement呢?但是除了createElement还有createElementNS,还有可能是页面上已经存在 iframe,所以不合适。
那就在每当新创建一个新 iframe 时,对setAttribute进行保护重写,这里又有用到MutationObserver:
/** * 使用 MutationObserver 对生成的 iframe 页面进行监控, * 防止调用内部原生 setAttribute 及 document.write * @return {[type]} [description] */ function defenseIframe() { // 先保护当前页面 installHook(window); } /** * 实现单个 window 窗口的 setAttribute保护 * @param {[BOM]} window [浏览器window对象] * @return {[type]} [description] */ function installHook(window) { // 重写单个 window 窗口的 setAttribute 属性 resetSetAttribute(window); // 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]; // 给生成的 iframe 里环境也装上重写的钩子 if (node.tagName == 'IFRAME') { installHook(node.contentWindow); } } }); }); observer.observe(document, { subtree: true, childList: true }); } /** * 重写单个 window 窗口的 setAttribute 属性 * @param {[BOM]} window [浏览器window对象] * @return {[type]} [description] */ function resetSetAttribute(window) { // 保存原有接口 var old_setAttribute = window.Element.prototype.setAttribute; // 重写 setAttribute 接口 window.Element.prototype.setAttribute = function(name, value) { ... }; }