大前端

为什么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>

防范措施

  1. 强制使用CSRF保护

使用这种方式可以在没有设置特定header信息或者 CSRF token 的时候,直接不返回数据。

  1. 强制使用 POST 方式来访问API。

从script标签的角度的特点来看,它是无法发送post请求的。而通过js代码发送的请求,无法隐式认证,因此这种方法可以防范攻击。

不过对于第二种方式来说,目前越来越流行的rest,已经赋予了get/post/put/delete一些语义的作用,因此强制不使用get不是一种好的解决方案。

总结

谷歌和Facebook的方式通过在返回的数据前加入死循环和非法字符,可以阻止上述通过script标签的形式来盗取数据(因为如果用script标签,获取的数据是会被当做代码来执行的,而执行的时候会直接死循环或者报错。)

虽然上述的方法可能在新的浏览器中都已经失效了,但是我们应该知道攻击者的一些思路,而且要尽最大可能来保护我们的API不被非法盗用。

这里还有一个相似的漏洞叫CSRF,它同样是利用了cookie的隐式认证,在恶意页面发出用户正常的请求,达到伪造操作的目的,两者一个是执行非法敏感操作,一个是收集敏感信息。

除了上面的这些非同源的攻击之外,还有一个东西叫做SOME(同源方法执行漏洞),原理上也有很多相似的地方