iframe解析

解密iframe中的秘密

Posted by Li Yucang on December 12, 2018

iframe 解析

基本概念

先来看个例子:


<iframe src="demo.html" height="300" width="500" name="demo" scrolling="auto" sandbox="allow-same-origin"></iframe>

iframe 常用属性:

  1. frameborder: 是否显示边框,1(yes),0(no)

  2. height: 框架作为一个普通元素的高度,建议在使用 css 设置。

  3. width: 框架作为一个普通元素的宽度,建议使用 css 设置。

  4. name: 框架的名称,window.frames[name]时专用的属性。

  5. scrolling: 框架的是否滚动。yes,no,auto。

  6. src: 内框架的地址,可以使页面地址,也可以是图片的地址。

  7. srcdoc: 用来替代原来 HTML body 里面的内容。但是 IE 不支持, 不过也没什么卵用

  8. sandbox: 对 iframe 进行一些列限制,IE10+支持

我们通常使用 iframe 最基本的特性,就是能自由操作 iframe 和父框架的内容(DOM). 但前提条件是同域. 如果跨域顶多只能实现页面跳转 window.location.href = xxx


A:<iframe id="mainIframe" name="mainIframe" src="/main.html" frameborder="0" scrolling="auto" ></iframe>
B:<iframe id="mainIframe" name="mainIframe" src="http://www.baidu.com" frameborder="0" scrolling="auto" ></iframe>

使用 A 时,因为同域,父页面可以对子页面进行改写,反之亦然。

使用 B 时,不同域,父页面没有权限改动子页面,但可以实现页面的跳转这里,我们先从简单的开始,当主页面和 iframe 同域时,我们可以干些什么。

获取 iframe 里的内容

主要的两个 API 就是 contentWindow 和 contentDocument:

iframe.contentWindow, 获取 iframe 的 window 对象。iframe.contentDocument, 获取 iframe 的 document 对象。这两个 API 只是 DOM 节点提供的方式(即 getELement 系列对象)。

var iframe = document.getElementById("iframe1");
var iwindow = iframe.contentWindow;
var idoc = iwindow.document/iframe.contentDocument;
       console.log("window",iwindow);//获取iframe的window对象
       console.log("document",idoc);  //获取iframe的document
       console.log("html",idoc.documentElement);//获取iframe的html
       console.log("head",idoc.head);  //获取head
       console.log("body",idoc.body);  //获取body

另外更简单的方式是,结合 Name 属性,通过 window 提供的 frames 获取。

<iframe src ="/index.html" id="ifr1" name="ifr1" scrolling="yes">
  <p>Your browser does not support iframes.</p>
</iframe>
<script type="text/javascript">
    console.log(window.frames['ifr1'] == document.getElementById("ifr1").contentWindow);
    // true
</script>

其实 window.frames['ifr1']返回的就是 window 对象,这里就看你想用哪一种方式获取 window 对象,两者都行,不过本人更倾向于第二种使用 frames[xxx].因为,字母少啊喂~ 然后,你就可以操控 iframe 里面的 DOM 内容。

在 iframe 中获取父级内容

同理,在同域下,父页面可以获取子 iframe 的内容,那么子 iframe 同样也能操作父页面内容。在 iframe 中,可以通过在 window 上挂载的几个 API 进行获取.

window.parent 获取上一级的window对象,如果还是iframe则是该iframe的window对象
window.top 获取最顶级容器的window对象,即,就是你打开页面的文档
window.self 返回自身window的引用。可以理解 window===window.self

使用 iframe 进行异步请求

话说在很久很久以前,我们实现异步发送请求是使用 iframe 实现的~! 怎么可能!!!

真的史料为证(自行 google), 那时候为了不跳转页面,提交表单时是使用 iframe 提交的。现在,前端发展尼玛真快,websocket,SSE,ajax 等,逆天 skill 的出现,颠覆了 iframe, 现在基本上只能活在 IE8,9 的浏览器内了。 但是,宝宝以为这样就可以不用了解 iframe 了,而现实就是这么残酷,我们目前还需要兼容 IE8+。所以,iframe 实现长轮询和长连接的 trick 我们还是需要涉猎滴。

iframe 提交表单

const requestPost = ({url, data}) => {
  // 首先创建一个用来发送数据的iframe.
  const iframe = document.createElement('iframe')
  iframe.name = 'iframePost'
  iframe.style.display = 'none'
  document.body.appendChild(iframe)
  const form = document.createElement('form')
  const node = document.createElement('input')
  // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  iframe.addEventListener('load', function () {
    // 这里可以这样获取返回内容
    console.log(iframe.contentDocument.body.innerText)
  })

  form.action = url
  // 在指定的iframe中执行form
  form.target = iframe.name
  form.method = 'post'
  for (let name in data) {
    node.name = name
    node.value = data[name].toString()
    form.appendChild(node.cloneNode())
  }
  // 表单元素需要添加到主文档中.
  form.style.display = 'none'
  document.body.appendChild(form)
  form.submit()

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
  document.body.removeChild(form)
}
// 使用方式
requestPost({
  url: 'http://localhost:9871/api/iframePost',
  data: {
    msg: 'helloIframePost'
  }
})

iframe 长轮询

如果写过 ajax 的童鞋,应该知道,长轮询就是在 ajax 的 readyState = 4 的时,再次执行原函数即可。 这里使用 iframe 也是一样,异步创建 iframe,然后 reload, 和后台协商好, 看后台哥哥们将返回的信息放在哪,然后获取里面信息即可. 这里是直接放在 body 里.

var iframeCon = docuemnt.querySelector('#container'),
        text; //传递的信息
var iframe = document.createElement('iframe'),
    iframe.id = "frame",
    iframe.style = "display:none;",
    iframe.name="polling",
    iframe.src="target.html";
iframeCon.appendChild(iframe);
iframe.onload= function(){
    var iloc = iframe.contentWindow.location,
        idoc  = iframe.contentDocument;
    setTimeout(function(){
        text = idoc.getElementsByTagName('body')[0].textContent;
        console.log(text);
        iloc.reload(); //刷新页面,再次获取信息,并且会触发onload函数
    },2000);
}

这样就可以实现 ajax 的长轮询的效果。 当然,这里只是使用 reload 进行获取,你也可以添加 iframe 和删除 iframe 的方式,进行发送信息,这些都是根据具体场景应用的。另外在 iframe 中还可以实现异步加载 js 文件,不过,iframe 和主页是共享连接池的,所以还是很蛋疼的,现在基本上都被 XHR 和 hard calllback 取缔了,这里也不过多介绍了。

iframe 跨域

iframe 就是一个隔离沙盒,相当于我们在一个页面内可以操控很多个标签页一样。有了解过的童鞋应该知道,iframe 解决跨域也是很有一套的。

iframe 跨域通讯之 document.domain

对于主域相同子域不同的两个页面,我们可以通过 document.domain + iframe 来解决跨域通信问题。举个 🌰,http: //www.foo.com/a.htmlhttp: //script.foo.com/b.html 两个文件中分别加上 document.domain = ‘foo.com’,指定相同的主域,然后,两个文档就可以进行交互。

//b.html是以iframe的形式嵌套在a.html中
//www.foo.com上的a.html
document.domain = 'foo.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://script.foo.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
    var doc = ifr.contentDocument || ifr.contentWindow.document;
    // 在这里操纵b.html
    alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue);
};
//script.foo.com上的b.html
document.domain = 'foo.com';

默认情况下 document.domain 是指 window.location.hostname. 你可以手动更改,但是最多只能设置为主域名。 通常,主域名就是指不带 www 的 hostname, 比如: foo.com , baidu.com 。 如果,带上 www 或者其他的前缀,就是二级域名或者多级域名。通过上述设置,相同的 domain 之后,就可以进行同域的相关操作。另外还可以使用 iframe 和 location.hash,不过由于技术 out 了,这里就不做介绍了。

iframe 跨域通讯之 postMessage

如果你设置的 iframe 的域名和你 top window 的域名完全不同。 则可以使用 CDM(cross document messaging)进行跨域消息的传递。该 API 的兼容性较好 ie8+都支持.

<iframe src="http://tuhao.com" name="sendMessage"></iframe>
//当前脚本
let ifr = window.frames['sendMessage'];
   //使用iframe的window向iframe发送message。
ifr.postmessage('give u a message', "http://tuhao.com");

//tuhao.com的脚本
window.addEventListener('message', receiver, false);
function receiver(e) {
    if (e.origin == 'http://tuhao.com') {
        if (e.data == 'give u a message') {
            e.source.postMessage('received', e.origin);  //向原网页返回信息
        } else {
            alert(e.data);
        }
    }
}

当 targetOrigin 接受到 message 消息之后,会触发 message 事件。 message 提供的 event 对象上有 3 个重要的属性:data、origin、source。

  1. data:postMessage 传递进来的值
  2. origin:发送消息的文档所在的域
  3. source:发送消息文档的 window 对象的代理,如果是来自同一个域,则该对象就是 window,可以使用其所有方法,如果是不同的域,则 window 只能调用 postMessage()方法返回信息

iframe 的安全问题

iframe 出现安全性有两个方面,一个是你的网页被别人 iframe,一个是你 iframe 别人的网页。 当你的网页被别人 iframe 时,比较蛋疼的是被钓鱼网站利用,然后 victim+s 留言逼逼你。所以为了防止页面被一些不法分子利用,我们需要做好安全性措施。

防嵌套网页

在前端领域,为了防止网站被钓鱼,可以使用 window.top 来防止你的网页被 iframe。

if(window != window.top){
    window.top.location.href = correctURL;
}

这段代码的主要用途是限定你的网页不能嵌套在任意网页内。如果你想引用同域的框架的话,可以判断域名。

if (top.location.host != window.location.host) {
  top.location.href = window.location.href;
}

当然,如果你网页不同域名的话,上述就会报错。所以,这里可以使用 try…catch…进行错误捕获。如果发生错误,则说明不同域,表示你的页面被盗用了。可能有些浏览器这样写是不会报错,所以需要降级处理。这时候再进行跳转即可.

try{
  top.location.hostname;  //检测是否出错
  //如果没有出错,则降级处理
  if (top.location.hostname != window.location.hostname) {
    top.location.href =window.location.href;
  }
}
catch(e){
  top.location.href = window.location.href;
}

这只是浏览器端,对 iframe 页面的权限做出相关的设置。 我们还可以在服务器上,对使用 iframe 的权限进行设置.

X-Frame-Options

X-Frame-Options 是一个响应头,主要是描述服务器的网页资源的 iframe 权限。目前的支持度是 IE8+(已经很好了啊喂)有 3 个选项:

  1. X-Frame-Options: DENY 拒绝任何 iframe 的嵌套请求

  2. X-Frame-Options: SAMEORIGIN 只允许同源请求,例如网页为 foo.com/123.php,則 foo.com 底下的所有网页可以嵌入此网页,但是 foo.com 以外的网页不能嵌入

  3. X-Frame-Options: ALLOW-FROM http://s3131212.com 只允许指定网页的 iframe 请求,不过兼容性较差 Chrome 不支持

X-Frame-Options 其实就是将前端 js 对 iframe 的把控交给服务器来进行处理。

//js
if(window != window.top){
    window.top.location.href = window.location.href;
}
//等价于
X-Frame-Options: DENY

//js
if (top.location.hostname != window.location.hostname) {
    top.location.href =window.location.href;
}
//等价于
X-Frame-Options: SAMEORIGIN

该属性是对页面的 iframe 进行一个主要限制,不过,涉及 iframe 的 header 可不止这一个,另外还有一个 Content Security Policy, 他同样也可以对 iframe 进行限制,而且,他应该是以后网页安全防护的主流。

CSP 之页面防护

内容安全策略(CSP)用于检测和减轻用于 Web 站点的特定类型的攻击,例如 XSS 和数据注入等。

通过 CSP 配置 sandbox 和 child-src 可以设置 iframe 的有效地址,它限制适 iframe 的行为,包括阻止弹出窗口,防止插件和脚本的执行,而且可以执行一个同源策略。

我们可以在 html 头部中加上<meta>标签


    <meta http-equiv="Content-Security-Policy" content="child-src 'unsafe-inline' 'unsafe-eval' www.easonwong.com">

或者通过 HTTP 头部信息加上 Content-Security-Policy 字段

具体这里不细说,详情见我的另一片关于 csp 的文章。

sandbox

sandbox 就是用来给指定 iframe 设置一个沙盒模型限制 iframe 的更多权限. sandbox 是 h5 的一个新属性,IE10+支持(md~). 启用方式就是使用 sandbox 属性:

<iframe sandbox src="..."></iframe>

这样会对 iframe 页面进行一系列的限制:

1. script脚本不能执行
2. 不能发送ajax请求
3. 不能使用本地存储,即localStorage,cookie等
4. 不能创建新的弹窗和window
5. 不能发送表单
6. 不能加载额外插件比如flash等

看到这里,我也是醉了。好好的一个 iframe,你这样是不是有点过分了。不过,你可以放宽一点权限。在 sandbox 里面进行一些简单设置

<iframe sandbox="allow-same-origin" src="..."></iframe>

常用的配置项有:

配置 效果
allow-forms 允许进行提交表单
allow-scripts 运行执行脚本
allow-same-origin 允许同域请求,比如 ajax,storage
allow-top-navigation 允许 iframe 能够主导 window.top 进行页面跳转
allow-popups 允许 iframe 中弹出新窗口,比如,window.open,target=”_blank”
allow-pointer-lock 在 iframe 中可以锁定鼠标,主要和鼠标锁定有关

可以通过在 sandbox 里,添加允许进行的权限.

<iframe sandbox="allow-forms allow-same-origin allow-scripts" src="..."></iframe>

这样,就可以保证 js 脚本的执行,但是禁止 iframe 里的 javascript 执行 top.location = self.location。

哎,其实,iframe 的安全问题还是超级有的。比如 location 劫持,Refers 检查等。 不过目前而言,知道怎么简单的做一些安全措施就 over 了,白帽子们会帮我们测试的。