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

Jayden Lin
程式猿吃香蕉
Published in
11 min readDec 11, 2018

筆者任職於 Yahoo ,粉絲團:《程式猿吃香蕉🍌 | 線上課程:《經典駭客攻擊教程:給每個人的網站安全入門》

無窮迴圈?發生什麼事?

前幾天跟同事討論到一篇有趣的文章,這篇文章討論到:為什麼在 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 要回傳無窮迴圈?

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

因為先加上那一段無窮迴圈,之後再擺上真正 API 回傳的內容,即使未來 Array 被覆寫,因為無窮迴圈在前面,所以後面的陣列 […] 永遠不會被執行到。

因為隨著 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
程式猿吃香蕉

曾在 Yahoo 擔任 Lead Engineer,負責廣告系統,帶團隊做跨國開發,現任職區塊鏈產業。也是《程式猿吃香蕉》團隊創辦人,喜歡將實用的軟體知識以簡單生動的方式講給大家聽 😄😄😄