WEB 安全:JSONP 沙箱技术实现
2005 年 12 月,Bob Ippolito 正式提出 JSONP,以实现跨域请求 JSON 数据。目前 JSONP 已经是业界流行的跨域数据获取方案。
JSONP 原理
它的原理是通过 <script> 标签实现跨域加载服务器动态输出的脚本,并执行指定函数回传数据。例子如下:
// 页面预先定义好函数
window._users = function(data) {
console.log(data);
}
<!--请求 JSONP 脚本-->
<script src="http://api.example.net/users/aui/?callback=_users"></script>
_users({
id: 'aui',
name: '糖饼',
github: 'https://github.com/aui'
})
安全问题
引用站外脚本会让任何内容有机会注入到网站中:
- 如果 JSONP 源被黑客攻陷,网站也可能会受到攻击。
- 如果传输过程中被运营商劫持,网站可能会弹出广告(这个问题非常具有中国特色)。
JSONP 到目前为止依然还是一个未列入标准的技术方案,有人提出定义 JSONP 严格安全子集,使浏览器可以对 MIME 类别是 application/json-p 请求做强制处理,严格限定 JSONP 的权限,但是目前依然没有得到浏览器支持。
沙箱技术
iframe
如果利用 <iframe> 创造一个新的执行环境来加载站外脚本,这样可以为 JSONP 提供一层简单的防御。
window._users = function(data) {
console.log(data);
}
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
var contentDocument = iframe.contentWindow.document;
contentDocument.open();
contentDocument.write('<html><head>' +
'<script>window._users = parent._users;</script>' +
'<script src="http://api.example.net/users/aui/?callback=_users"></script>' +
'</head><body></body></html>');
contentDocument.close();
但是 <iframe> 并非与页面完全隔离,攻击者使用 top 、 parent 等变量可以轻易的拿到页面的全局对象,使得防护失效。
HTML5 sandbox
HTML5 为 <iframe> 提供了 sandbox 属性,通过指令来控制 <iframe> 的权限。sandbox 开启后会隔离与父页面访问,页面与 <iframe> 通讯可以使用 HTML5 parent.postMessage(message, targetOrigin) 通讯。
window._users = function(data) {
console.log(data);
}
window.onmessage = function(event) {
var data = event.data;
window._users(data);
};
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.sandbox = 'allow-scripts';
iframe.srcdoc = '<html><head>' +
'<script>window._users = function(data) {'+
'parent.postMessage(data, "*")' +
'};</script>' +
'<script src="http://api.example.net/users/aui/?callback=_users"></script>' +
'</head><body></body></html>';
document.body.appendChild(iframe);
启用 sandbox 后,如果 JSONP 脚本窃取 Cookie 或者篡改父页面都会被浏览器阻止。
IE 与 sandbox
IE 只有 IE10 以及更高版本支持 sandbox,在低版本 IE 浏览器中可以使用重写技术(override)来实现 sandbox 特性:重写 top、 parent、 docuemnt 等危险全局变量,使得外部脚本无法获取其引用。
黑魔法
在 IE 中删除、重写内置全局变量,常规的操作都将会失败,例如:
delete window.parent;
window.parent = undefined;
this.parent = undefined;
var parent = undefined;
console.log(parent.toString()); // [object Window]
但定义同名函数却可以覆盖,这也是目前已知的唯一方式。
function parent(){}
console.log(parent.toString()); // function parent(){}
由于函数声明后会被“提升”,从而无法引用原始全局变量。
var host = parent;
function parent(){}
console.log(host.toString()); // function parent(){}
好在 IE 有 execScript() 这个独有的方法可以在全局环境编译字符串运行,结合闭包便可以私有化指定的全局变量。
(function(parent) {
if (window.execScript) {
execScript('function parent(){}');
}
console.log(parent.toString()); // [object Window]
})(parent);
console.log(parent.toString()); // function parent(){}
这样只有内部函数可以获取到父页面引用进行跨窗口通讯,而外部脚本无论如何也拿不到真正的 parent 了,从而实现与父窗口隔离。
为了安全起见还可以全部重写 window 可枚举的属性以绝后患。
if (window.execScript) {
var code = [];
var blackList = window;
for (var key in blackList) {
code.push('function ' + key + '(){}');
}
code = code.join('');
execScript(code);
}
对于无法枚举的危险全局变量可以显式声明黑名单,禁止 IE 浏览器发送通讯请求,因为这样会带上站内 Cookie 从而引发安全风险。
blackList.Image = true;
blackList.ActiveXObject = true;
blackList.XMLHttpRequest = true;
blackList.execScript = true;
上述代码似乎一切都考虑妥当了?然而 IE9 却有个致命的问题:document 与 location 是常量,无法被覆盖。
document
由于 <iframe> 的 Cookie 是与父页面共享的,也就是说通过 document.cookie 可以轻松窃取到 Cookie。
document.cookie = 'hello world';
delete document.cookie;
console.log(document.cookie); // hello world
好在 IE9 可以访问 document 的构造函数,而 document.cookie 是其原型上的一个 "getter"、"setter" 属性,如果删除原型的对应属性即可让其失效。document 的继承关系为:HTMLDocument->Document->Node,清理其原型链即可。
function removeProto(name) {
var controller = window[name];
if (controller) {
var proto = controller.prototype;
for (var key in proto) {
if (key !== 'close') {
try {
delete proto[key];
} catch (e) {}
}
}
}
}
removeProto('Node');
removeProto('Document');
removeProto('HTMLDocument');
console.log(document.cookie); // undefined
console.log(document.body); // undefined
console.log(document.createElement); // undefined
至此,IE9 下 document 已经残废,其不但无法访问 document.cookie 甚至连 document.createElement 等基础 DOM API 都无法使用了。
location
由于 location 无法通过删除原型链的方式进行破坏,这意味着外部脚本有机会将 <iframe> 内部页面导航到新的地址中去。
location = 'http://hacker.com/goto.html';
<iframe> 被重载后,沙箱内的环境也将被重置,所有权限都将会被泄漏。
<!-- http://hacker.com/goto.html -->
<script>
top.location = 'http://fishing.com';
</script>
虽然无法破坏 location,IE 却可以使用 unload 事件中实现沙箱“自杀”,从而阻止安全风险。
if (window.addEventListener) {
addEventListener('unload', killSandbox);
} else {
attachEvent('onunload', killSandbox);
}
function killSandbox() {
location.href = 'about:blank';
}
经过周密防范后,运行在此 <iframe> 的外部脚本几乎只有运行回调函数的权限,达到目标。
后记
基于 sandbox 的 JSONP 加载器代码已经开源:https://github.com/aui/jsonp-sandbox
JSONP.get('http://api.example.net/users/aui', function (data) {
console.log(data);
});
值得借鉴。 top.document.getElementById('code') = false; //这个写得太急了吧 throw Error('沙箱失效!');
@scscms 已经修复
IE8下:先把top.location = './inc/proxy.html';改成location.href = './inc/proxy.html';
在inc/proxy.html仍可访问top
var div = document.createElement("div"); div.innerHTML = "我是广告"; top.document.appendChild(div);
@scscms 已经修复:
if (window.addEventListener) {
addEventListener('unload', killSandbox);
} else {
attachEvent('onunload', killSandbox);
}
function killSandbox() {
location.href = 'about:blank';
}
系统:iOS 微信版本号:6.3.30
微信里的浏览器支持拥有iframe的sandbox和srcdoc属性,却不支持设置srcdoc
Safari 以及其他第三方 App webview 正常,这可能是微信 webview 禁用了此特性
在IE几个版本下好像并没有解决请求的js含有location.href="http://www.x.com/1.html"问题,1.html可以正常访问top,parent(注意jsonp可只返回locaton.href="...") 另:location.href="xx"在ie10及11下只能使用beforeunload监听(刷新可用unload),但并不能阻止其跳转!!