为什么Facebook的API返回值以死循环开头?
假如你从浏览器中查看过一些大公司的 API 返回值,你会发现在 API 返回的 JSON 前面加了一些奇怪的 JavaScript 代码:
Facebook 的 API :
Gmail 的 API :
那么他们为什么要浪费几个字节来让自己的 JSON 无效?
答案是:保护你的数据!
假如没有这几个重要的字节,任何其他的外部网站都可能访问你的数据。
这种攻击方式被称作 JSON hijacking ,外部网站可以通过该手段来盗取 JSON 中的数据。
问题起源
在 JavaScript 1.5 及之前的版本中,程序是允许覆盖原始类型的构造方法的,而且使用括号表达式的时候,覆盖后的构造方法会被调用。
也就是说下面的代码:
function Array(){
alert('You created an array!');
}var x = [1,2,3];
会在执行后弹出一个框!
因此,攻击者只要首先覆盖原始类型的构造方法,然后通过加载外部脚本的方式加载接口数据,别人就可以读取你的Email了,例如下面的伪代码:
<script>function Array() { // 读取Array的内容,并且提交给攻击者后台}</script>// 由于gmail接口会返回一个数组,因此会初始化,并且调用攻击者覆盖的构造函数<script src="https://gmail.com/messages"></script>
假如你之前登录过 Gmail , cookie中会保存之前登录的凭据,所以下面的script标签是能正常取到数据的。
数据盗取
尽管你覆盖了构造函数,但是数组仍然会被初始化,而且你可以通过this来对数组进行访问。
下面的代码段会弹出数组中的所有元素:
function Array() { var that = this; var index = 0; // 通过覆盖数组的setter方法,当数据被加入数组中时弹出该值
var valueExtractor = function(value) { // 弹出数值
alert(value); // 将下一个数值的setter方法修改为我们自定义的方法
that.__defineSetter__(index.toString(),valueExtractor );
index++;
}; // 为数组的第0个元素设置setter方法,当arr[0]=xxx时会被调用。
that.__defineSetter__(index.toString(),valueExtractor );
index++;
}
通过上面的方法,在创建数组的时候,这些值就会被弹出来。
上面的方式已经在 ECMAScript 4 提案中被修复,在新的提案中我们将不能覆盖这些基础类型的构造方法,比如 Object 或 Array 。
虽然 ECMAScript 4 还没有被正式推出,但是各大主流浏览器厂商在问题一经发现后就立即修复了该漏洞。
在今天你仍然可以在JavaScript中覆盖这些构造方法,但是仅在你使用构造方法构造对象时会被调用,而且这些对象不能是通过括号表达式构造的。
例如下面的采用Array关键词方式创建的数组:
function Array(){ console.log(arguments);
}Array("secret","values");
在最新的浏览器中,你会发现,数组中的数据仍然被输出出来,功能仍和之前相同。
主流浏览器的修复并不阻止你使用你自己定义的Array来创建对象,但是会强制括号表达式来使用 native 的实现方式来创建对象。
这也就意味着虽然我们创建了一个 Array 的构造方法,但是使用括号表达式(例如[1,2,3]
)创建数组的时候并不会调用该方法。
尽管我们使用 x = new Array(1,2,3)
或者 x = Array(1,2,3)
的时候我们自定义的方法会被调用,但是这样就避免了 JSON hijacking 的问题。
新的变种
上面的问题只会出现在旧的浏览器中,这对于我们现在来说有什么用处呢?
随着最新的 ECMAScript 6 的推出, 新的特性也被加了进来,比如Proxy代理。
使用代理可以不覆盖原有的方法而访问数据,下面是一种攻击方式,虽然在最新的浏览器中已经被修复,但是在某些版本的Edge中仍然可用。
<script>Object.setPrototypeOf(__proto__,new Proxy(__proto__,{ has:function(target,name){
alert(name);
}
}));</script><script src="external-script-with-undefined-variable"></script>
防范措施
- 强制使用CSRF保护
使用这种方式可以在没有设置特定header信息或者 CSRF token 的时候,直接不返回数据。
- 强制使用 POST 方式来访问API。
从script标签的角度的特点来看,它是无法发送post请求的。而通过js代码发送的请求,无法隐式认证,因此这种方法可以防范攻击。
不过对于第二种方式来说,目前越来越流行的rest,已经赋予了get/post/put/delete一些语义的作用,因此强制不使用get不是一种好的解决方案。
总结
谷歌和Facebook的方式通过在返回的数据前加入死循环和非法字符,可以阻止上述通过script标签的形式来盗取数据(因为如果用script标签,获取的数据是会被当做代码来执行的,而执行的时候会直接死循环或者报错。)
虽然上述的方法可能在新的浏览器中都已经失效了,但是我们应该知道攻击者的一些思路,而且要尽最大可能来保护我们的API不被非法盗用。
这里还有一个相似的漏洞叫CSRF,它同样是利用了cookie的隐式认证,在恶意页面发出用户正常的请求,达到伪造操作的目的,两者一个是执行非法敏感操作,一个是收集敏感信息。
除了上面的这些非同源的攻击之外,还有一个东西叫做SOME(同源方法执行漏洞),原理上也有很多相似的地方