為何 Facebook API 要回傳無窮迴圈? 談捲土重來的 JSON Hijacking

Jayden Lin
Dec 11, 2018 · 10 min read

無窮迴圈?發生什麼事?

前幾天跟同事討論到一篇有趣的文章,這篇文章討論到:為什麼在 Facebook 的頁面,API 回傳的 HTTP Response 開頭會是一段 for loop 無窮迴圈?

身為一個工程師,手滑開一下 Chrome Developer Tool 驗證一下也是很正常的。是的,你沒看錯。結果就像下圖所示一樣,在某隻 API 的 HTTP Response 裡面,出現了無窮迴圈:

Facebook API 的 HTTP Response

此外,除了 Facebook 之外,Google 同樣也會在 API 回傳的 HTTP Response 前面多加一些奇怪的字串,舉例來說:下圖是 Gmail 中呼叫 API 時產生的 HTTP Response。

Gmail API 的 HTTP Response

這跟 JSON Hijacking 有什麼關係?

先說結論:對於許多工程師來說,JSON Hijacking 這件事,隨著瀏覽器的進步,似乎已經離我們很遙遠了。然而,隨著 ES 6 的新功能, JSON Hijacking 又捲土重來了!而 Facebook 便是透過加上一個 For loop 無窮迴圈來阻擋 JSON Hijacking。


什麼是 JSON Hijacking ?

簡單來說,便是駭客透過覆寫 Array 這個函式,來達到偷取你 API 資料的目的。你可能會說:我的 API 都有經過 Server 端驗證使用者的 Cookie,應該很安全才對。然而, JSON Hijacking 是可以突破這樣的防禦的。

舉例來說,一家銀行做了一個 GET 的 API 回傳使用者的帳戶資料:

GET https://mybank.com/users/balance
[
{
"userName": "jayden",
"balance": 1200
},
{
"userName": "oscar",
"balance": 1200000
}]
Cookie: XXXXXOOOXXXXX

這隻 API 會回傳使用者們的帳戶還剩下多少錢,並驗證 Cookie ,確認是不是已登入的 「銀行經理」才能夠看到這些資料。

然而,駭客可以透過製作一個網頁 hacker.com,在該網頁中覆寫原生的 Array 函式,當「銀行經理」一進入駭客製作的網頁,駭客製作的 Array() 就可以讀到 API 回傳的資料,駭客只要在這段惡意程式碼裡面,將資料讀取後傳到駭客的主機就可以偷走資料了。

<script>function Array(){
// 覆寫原生的 Array 函式,這裡面放惡意程式碼
// 這裡的範例只是將資料 alert
var that = this;
var index = 0;
var valueExtractor = function(value) {
// Alert the value
alert(value);
// Set the next index to use this method as well
that.__defineSetter__(index.toString(),valueExtractor );
index++;
};
// Set the setter for item 0
that.__defineSetter__(index.toString(),valueExtractor );
index++;
}

<script>
<script src="https://mybank.com/users/balance"></script>

上面的範例,駭客透過 __defineSetter__來覆寫掉原本的 Array 的行為。


為什麼 JSON Hijacking 會生效?

以下一一來解釋為什麼這樣的攻擊可以生效。

1. Cookie 不是會驗證嗎?

當「銀行經理」點入的是駭客的網站 hacker.com時,駭客的程式碼 <script src="https://mybank.com/users/balance"></script> 會發送 GET 請求到 mybank.com,根據同源政策,即使「銀行經理」點入的是駭客的網站 hacker.com,與 mybank.com 的 domain 不同,「銀行經理」的瀏覽器也會一並將「銀行經理」的瀏覽器中 mybank.com 的 Cookie 送到 https://mybank.com/users/balance,造成驗證通過。(不了解同源政策的朋友,可以參考我之前的文章:Same Origin Policy 同源政策 ! 一切安全的基礎)

2. 同源政策不是會阻止 JavaScript 做跨來源讀取(cross-origin read)嗎 ?

可能有些人會說:好,我知道 Cookie 認證會過,但是根據同源政策,hacker.com 的 JavaScript,是無法直接讀 mybank.com 下載的內容的。所以,即使https://mybank.com/users/balance 的內容,因為 Cookie 認證通過而被載入,駭客的 JavaScript ,也就是被覆寫的 Array(),也無法讀出來才對。

然而有關跨來源讀取(cross-origin read),若是使用 <script> 標籤,瀏覽器會根據回傳的內容,來決定要不要擋下(可參考這裡的定義)。簡單來說,若API 回傳的內容是「可執行的 JavaScript」,則使用 <script> 標籤載入的內容,不受同源政策跨來源讀取(cross-origin read)的限制。

又因為 Array 這個函式被覆寫。瀏覽器並不知道它呼叫的 Array 已經是被駭客覆寫過的,造成攻擊發生。

3. 被覆寫的 Array 在哪裡被呼叫,我怎麼沒看到 ?

可能有些人會說,我沒有看到程式碼去讀取 https://mybank.com/users/balance 的 HTTP Response,也沒有看到呼叫 Array 的地方,到底是哪裡呼叫了 Array 這個函式?

這邊要特別說明,一段以 [] 開頭的 JavaScript 是可以直接被執行的,這也是造成同源政策無法防禦的原因。所以,如果你的 API 直接回傳 [] 形式的內容,例如:

[{},{},{}]

是可以直接被瀏覽器執行的,這邊要特別提一下,如果是以 {} 開頭的 JavaScript 是無法直接被執行的,例如:

{"key": "value"}

有興趣的朋友可以試試看,透過 eval 這個函式來做實驗。

eval('{"x":"y"}');  //會有 Error
eval('[1,2,3]'); //不會有 Error

所以,回到剛才的問題:Array 是在哪裡被呼叫的呢?

<script src="https://mybank.com/users/balance"></script> 載入 HTTP Response 裡的陣列到瀏覽器裡的時候,因為內容是:

[
{
"userName": "jayden",
"balance": 1200
},
{
"userName": "oscar",
"balance": 1200000
}]

瀏覽器解析 [] 的時候,Array 就會被呼叫了,駭客覆寫的 Array 就是在這個時候被呼叫了。


捲土重來的 JSON Hijacking ?

相信經過剛才的講解,大家都能夠理解 JSON Hijacking 的原理了,主要有三個重點:

  1. 陣列 [] 開頭的內容是可以直接被執行的 JavaScript,繞過同源政策的限制
  2. 只透過 Cookie 來驗證使用者是不足以保護你的 API 資料的
  3. 駭客覆寫Array()來造成攻擊生效

這個漏洞被爆出來之後,許多主流的瀏覽器,都迅速的修復掉了,讓駭客無法透過__defineSetter__來覆寫 Primitive Type 的 JavaScript 物件,例如:Array。

可是!JavaScript 這個語言,也是會隨著時間改變的,ES 6 推出了 Proxy 的功能,造成 Array 又能夠被駭客覆寫了。

以下是範例的程式碼:

<script>
Object.setPrototypeOf(__proto__,new Proxy(__proto__,{
has:function(target,name){
alert(name.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff); }));
}
}));
</script>
<script charset="UTF-16BE" src="external-script-with-array-literal"></script>

目前這個漏洞,也已被主流瀏覽器修掉,目前我自己在 Chrome 嘗試會出現錯誤:

有興趣了解程式碼細節的朋友,可以參考這一篇文章


為何 Facebook API 要回傳無窮迴圈?

在完整了解 JSON Hijacking 的原理之後,回到一開始的問題,為何 Facebook API 要回傳無窮迴圈?

相信可能有很多朋友猜到了:

因為隨著 JavaScript 功能的演進,也許未來哪一天 Array 又被什麼奇巧的方式覆寫了也說不定。

那麼你可能會有疑問,加上那段無窮迴圈之後,API 的回應內容,在網站客戶端 JavaScript 要怎麼處理?

答案是:移除掉那一段之後再做 JSON.parse。

有興趣的朋友,可以參考這篇文章的問答:https://dev.to/snorfalorpagus/comment/6oaa


OWASP 建議的防禦方式

Facebook 跟 Google 的防禦方式雖然有效,但乍看之下有點詭異,這邊也提供 OWASP 建議的防禦方式,OWASP 全名是 Open Web Application Security Project ,它是一個開放社群的非營利組織,致力於改善網站應用程式的安全性。

  1. 重要的資料要用 CSRF token 防禦
  2. API 回傳不要使用直接使用 JSON 陣列 [],改用 JSON 物件 {}

還記得剛出社會的時候,有被前輩建議過 API 資料不要直接回傳陣列,當時還不知其所以然,後來才知道這跟安全性也有相關。


Jayden Lin

Written by

Yahoo 資深軟體工程師,負責搜尋廣告系統的開發工作。曾擔任樂天市場前端開發組經理,負責從零建立樂天台灣前端開發團隊。同時也是 《Hey, 軟體知識沒有你想得那麼難》團隊創辦人,致力於提供優質的、入門的、科普性質的軟體知識給大家。

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade