最近在开发天码营前端预览工具(类似JSFiddle), 需要将CSS、HTML与JavaScript一起注入到ifame中。 借此研究一下iframe中注入脚本的不同方式之间的区别。

访问iframe内容

在父窗口中可以通过iframe DOM节点获取其windowdocument(需要同域)。 获取iframe的window对象(所有全局变量、函数都在该对象下):

var iframe = $('iframe').get(0);
var iwindow = $('iframe').prop('contentWindow');
// 相当于
var iwindow = document.getElementsByTagName('iframe')[0].contentWindow;

获取iframe的document有两种写法:

var idocument = $('iframe').prop('contentDocument');
// 相当于
var idocument = $('iframe').prop('contentWindow').document;

contentDocument属性在 IE 8 才开始支持,为了支持 IE 7,推荐使用contentWindow.document

通过appendChild注入

实现方式

iframe中脚本的上下文无非两种:1)与父元素共享window,2)与父元素隔离的window。 为了检查该上下文,我们设置父元素window.id并在iframe中打印出来。

window.id = 'parent window';
var injected_script = 'console.log("window.id ==", window.id)';

通过上述idocument可以访问iframe的DOM,借用appendChild()API来注入脚本:

var el = idocument.createElement('script');
el.text = injected_script;
idocument.body.appendChild(el);

执行结果

window.id == undefined

脚本样式文件被正常载入并执行,脚本的运行上下文也是与父容器隔离的。

注意:使用jQuery.append()方法注入:$(idocument.body).append(el), 上下文总是当前jQuery所在的window,因为jQuery总是用eval来执行注入的脚本。 见:jQuery2.2 iframe 脚本注入的上下文 Bug

缺点

难以显示完整的HTML。因为appendChilddocument.body的方法, 如果要在<head>下添加元素或操作整个<html>则会比较困难。

另外,只能与同域iframe进行互操作。

通过innerHTML注入

实现方式

DOM元素都有innerHTML属性可以设置其HTML内容, 我们利用body.innerHTML即可注入脚本。

var html = "<script>" + injected_script + "</script>";
idocument.body.innerHTML = html;

执行结果


上述代码没有任何输出,因为以innerHTML注入的HTML中的脚本不会执行。 你可能知道为<script>设置innerHTML也不会执行脚本内容,需要设置script.text属性才可以执行。 设置innerHTML的方法只适用于给DOM注入文本内容。

缺点

注入内容中的脚本不会被执行。如果非要执行的话,需要为所有脚本一一设置script.text属性,非常麻烦。

另外,只能与同域iframe进行互操作。

通过data src注入

实现方式

<img>标签接受data URI类型的src,你一定见过!data URI的语法如下:

data:[<mime type>][;charset=<charset>][;base64],<encoded data>

同样地,<iframe>也可以设置data类型的src属性, 这是iframe页面内容和脚本注入最通用最健壮的办法。

var html = '<script>' + injected_script + '</script>';
var html_src = 'data:text/html;charset=utf-8,' + encodeURI(html);
iframe.src = html_src;

执行结果

window.id == undefined

脚本样式文件被正常载入并执行,脚本的运行上下文也是与父容器隔离的。

另外,通过Data URI设置内容会使得iframe与容器跨域,没有脚本注入问题,但也不允许互操作。

缺点

跨域问题

由于设置了src,iframe和父容器是跨域的。 在父容器的上下文中,无法通过contentWindow.document访问iframe内容

Uncaught DOMException: Blocked a frame with origin "https://xxx" from accessing a cross-origin frame.

IE不兼容

在Microsoft IE中,只有五类DOM元素可以设置Data URI:

  • object (images only)
  • img
  • input type=image
  • link
  • CSS declarations that accept a URL, such as background, backgroundImage, and so on.

为iframe设置Data URI之后iframe会显示『无法显示该页面』。

参见:https://msdn.microsoft.com/en-us/library/cc848897(v=VS.85).aspx

通过document.write注入

实现方式

使用DOM APIdocument.open()方法打开并擦除一个文档, 然后调用document.write()写入内容, 最后调用document.close()关闭文档,迫使文档进行渲染和显示。

idocument.open();
idocument.write($html.prop('outerHTML'));
idocument.close();

参见:http://www.w3school.com.cn/jsref/met_doc_open.asp

执行结果

window.id == undefined

脚本样式文件被正常载入并执行,脚本的运行上下文也是与父容器隔离的。 document.write的方式主流浏览器均可支持,包括:MSIE、Chrome、Safari、Firefox。

缺点

通过DOM API互操作,要求iframe与父容器是同域的。 与前面所有DOM操作的注入方式同样会存在XSS安全问题。

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/04/14/iframe-script-injection.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。