前言
JSON(JavaScript Object Notation),可以說是事實的浏覽器,伺服器交換資料的标準了。目測其它的格式如XML,或者其它自定義的格式會越來越少。
為什麼JSON這麼流行?
和JavaScript無縫對接是一個原因。
還有一個重要原因是可以比較輕松的實作跨域。如果是XML,或者其它專有格式,則很難實作跨域,要通過flash之類來實作。
任何一種資料格式,如何解析處理不當,都會存在安全漏洞。下面扯談下JSON相關的一些安全東東。
在介紹之前,先來提幾個問題:
- 為什麼XMLHttpRequest要遵守同源政策?
- XMLHttpRequest 請求會不會帶cookie?
- <script scr="..."> 的标簽請求會不會帶cookie?
- 向一個其它域名的網站送出一個form,會不會帶cookie?
- CORS請求能不能帶cookie?
JSON注入
有的時候,可能是為了友善,有人會手動拼接下JSON,但是這種随手代碼,卻可能帶來意想不到的安全隐患。
第一種方式,利用字元串拼接:
String user = "test01";
String password = "12345', admin:'true";
String json = "{user:'%s', password:'%s'}";
System.out.println(String.format(json, user, password));
//{user:'test01', password:'12345', admin:'true'}
使用者增加了管理者權限。
第二種,利用Parameter pollution, 類似http parameter pollution
String string = "{user:'test01',password:'hello', password:'world'}";
JSONObject parse = JSON.parseObject(string);
String password = parse.getString("password");
System.out.println(password);
//world
當JSON資料key重複了會怎麼處理?大部分JSON解析庫都是後面的參數覆寫了前面的。
下面的示範了修改别人密碼的例子:
//user%3Dtest01%26password%3D12345%27%2Cuser%3Dtest02"
//user=test01&password=12345',user=test02
HttpServletRequest request = null;
String user = request.getParameter("user");
//檢查test01是否登陸
String password = request.getParameter("password");
String content = "{user:'" + user + "', password:'" + password + "'}";
User user = JSON.parseObject(content, User.class);
//{"password":"12345","user":"test02"}
updateDb(user);
是以說,不要手動拼接JSON字元串
浏覽器端應該如何處理JSON資料?
像eval這種方式,自然是不能采用的。
現在的浏覽器都提供了原生的方法 JSON.parse(str) 來轉換為JS對象。
如果是IE8之前的浏覽器,要使用這個庫來解析:https://github.com/douglascrockford/JSON-js
參考:http://zh.wikipedia.org/wiki/JSON#.E5.AE.89.E5.85.A8.E6.80.A7.E5.95.8F.E9.A1.8C
JQuery裡内置了JSON解析庫
JSONP callback注入
簡單介紹jsonp的工作原理。
jsonp工作原理:
用js在document上插入一個<script>标簽,标簽的src指向遠端伺服器的API位址。用戶端和伺服器約定一個回調函數的名字,然後伺服器傳回回調函數包裹着的資料,然後浏覽器執行回調函數,取得資料。
比如jquery是這樣子實作的:
var url = 'http://localhost:8080/testJsonp?callback=?';
$.getJSON(url, function(data){
alert(data)
});
jquery自動把?轉成了一個帶時間戳特别的函數(防止緩存):
http://localhost:8080/testJsonp?callback=jQuery1102045087050669826567_1386230674292&_=1386230674293
相當于插入了這麼一個<script>标簽:
<script src="http://localhost:8080/testJsonp?callback=jQuery1102045087050669826567_1386230674292&_=1386230674293"></script>
伺服器傳回的資料是這樣子的:
jQuery1102045087050669826567_1386230674292({'name':'abc', 'age':18})
浏覽器會執行直接執行這個JS函數。
是以,如果在callback函數的名字上做點手腳,可以執行任意的JS代碼。是以說callback名字一定要嚴格過濾。
當然,callback函數的名字通常是程式自己控制的,但是不能排除有其它被利用的可能。
那麼callback函數的名字,如何過濾?應當隻允許合法的JS函數命名,用正則來比對應該是這樣子的:
^[0-9a-zA-Z_.]+$
正則可能比較慢,可以寫一個函數來判斷:
static boolean checkJSONPCallbackName(String name) {
try {
for (byte c : name.getBytes("US-ASCII")) {
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')
{
continue;
} else {
return false;
}
}
return true;
} catch (Throwable t) {
return false;
}
}
實際上對callback函數名字進行嚴格檢驗還有其它的一個好處,就是防範了很多UTF-7編碼攻擊。
因為UTF-7編碼的頭部都是帶有特殊字元的,如"+/v8","+/v9",這樣就過濾掉非法編碼的請求了。
update: 2016-3-22
spring 4.1裡有一個AbstractJsonpResponseBodyAdvice ,裡面是用正則比對來判斷是否合法的jsonp函數名。
public abstract class AbstractJsonpResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice {
/**
* Pattern for validating jsonp callback parameter values.
*/
private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");
jsonp的請求的驗證
jsonp在使用的時候,還有容易犯的錯誤是沒有驗證使用者的身份。
第一,操作是否是使用者自己送出的,而不是别的網頁用<script>标簽,或者用<form>送出的
是以要檢查request的refer,或者驗證token。這個實際是CSRF防護的範疇,但是很容易被忽略。
第二,要驗證使用者的權限。
很多時候,可能隻是驗證了使用者是否登入 。卻沒有仔細判斷使用者是否有權限。
比如通過JSONP請求修改了别的使用者的資料。
是以說,一定要難證來源。判斷refer,驗證使用者的身份。
到烏雲上搜尋,可以找到不少類似的漏洞,都是因為沒有嚴格驗證使用者的權限。
http://www.wooyun.org/searchbug.php?q=jsonpJSON hijacking
在JS裡可以為對象定義一些setter函數,這樣的話就存在了可以利用的漏洞。
比如在浏覽器的JS Console裡執行:
window.__defineSetter__('x', function() {
alert('x is being assigned!');
});
window.x=1;
會很神奇地彈出一個alert視窗,說明我們定義的setter函數起作用了。
結合這個,當利用<script>标簽請求外部的一個JSON API時,如果傳回的是數組型,就可以利用竊取資料。
比如有這樣的一個API:
http://www.test.com/friends
傳回的資料是JSON Array:
[{'user':'test01','age':18},{'user':'test02,'age':19},{'user':'test03','age':20}]
在攻擊頁面上插入以下的代碼,就可以擷取到使用者的所有的朋友的資訊。
<script>
Object.prototype.__defineSetter__('user',function(obj)
{alert(obj);
}
);
</script>
<script src="http://www.test.com/friends"></script>
這個漏洞在前幾年很流行,比如qq郵箱的一個漏洞:http://www.wooyun.org/bugs/wooyun-2010-046
現在的浏覽器都已經修複了,可以下載下傳一個Firefox3.0版本來測試下。目前的浏覽器在解析JSON Array字元串的時候,不再去觸發setter函數了。但對于object.xxx 這樣的設定,還是會觸發。
IE的utf-7編碼解析問題
這個漏洞也曾經很流行。利用的是老版的IE可以解析utf-7編碼的字元串或者檔案,繞過伺服器的過濾。舉個烏雲上的例子:http://www.wooyun.org/bugs/wooyun-2011-01293
有這樣的一個jsonp調用接口:
http://jipiao.taobao.com/hotel/remote/livesearch.do?callback=%2B%2Fv8%20%2BADwAaAB0AG0APgA8AGIAbwBkAHkAPgA8AHMAYwByAGkAcAB0AD4AYQBsAGUAcgB0ACgAMQApADsAPAAvAHMAYwByAGkAcAB0AD4APAAvAGIAbwBkAHkAPgA8AC8AaAB0AG0APg
url decoder之後是:
http://jipiao.taobao.com/hotel/remote/livesearch.do?callback=+/v8 +ADwAaAB0AG0APgA8AGIAbwBkAHkAPgA8AHMAYwByAGkAcAB0AD4AYQBsAGUAcgB0ACgAMQApADsAPAAvAHMAYwByAGkAcAB0AD4APAAvAGIAbwBkAHkAPgA8AC8AaAB0AG0APg
因為jsonp調用是直接傳回callback包裝的資料,是以實際上,上面的請求直接傳回的是:
+/v8 +ADwAaAB0AG0APgA8AGIAbwBkAHkAPgA8AHMAYwByAGkAcAB0AD4AYQBsAGUAcgB0ACgAMQApADsAPAAvAHMAYwByAGkAcAB0AD4APAAvAGIAbwBkAHkAPgA8AC8AaAB0AG0APg-(調用結果資料)
IE做了UTF-7解碼之後資料是這樣子的:
<htm><body><script>alert(1);</script></body></htm>(調用結果資料)
于是,就執行了XSS。
另外用IFrame也是可以的。但是我在IE8上測試,url的字尾需要是html才會觸發。
IE把沒有聲明傳回Content-Type的請求當做了"text/html"類型的,然後解析就有問題了。隻要伺服器端顯式設定了Content-Type為"application/json",則IE不會識别編碼,就不會觸發漏洞。是以說伺服器端的Content-Type一定要設定對。盡管設定之後調試有點麻煩,但是卻大大提高了安全性。
JSON格式設定為:"application/json"
JavaScript設定為:"application/x-javascript"
JavaScript還有一些設定為:"text/javascript"等,都是不規範的。
其它的一些東東
MongoDB注入
這個實際上就是JSON注入,簡單的字元串拼接,可能會引發各種資料被修改的問題。
JSON解析庫的問題
有些JSON庫解析庫支援循環引用,那麼是否可以構造特别的資料,導緻其解析失敗?進而引起CPU使用過高,拒絕服務等問題?
FastJSON的一個StackOverflowError Bug:
https://github.com/alibaba/fastjson/issues/76
有些JSON庫解析有問題:
http://www.freebuf.com/articles/web/10672.html
JSON-P
有人提出一個
的規範,但是貌似目前都沒有浏覽器有支援這個的。
原理是對于JSONP請求,浏覽器可以要求伺服器傳回的MIME是"application/json-p",這樣可以嚴格校驗是否合法的JSON資料。
CORS(Cross-Origin Resource Sharing)
為了解決跨域調用的安全性問題,目前實際上可用的方案是CORS:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
http://www.w3.org/TR/cors/
原理是通過伺服器端設定允許跨域調用,然後浏覽器就允許XMLHttpRequest跨域調用了。
CORS可以發起GET/POST請求,不像JSONP,隻能發起GET請求。
預設情況下,CORS請求是不帶cookie的。
我個人認為,這個方案也很蛋疼,一是需要伺服器配置,二是協定複雜。浏覽器如果不能确定是否能夠跨域調用,還要先進行一個Preflight Request。。
實際上,即使伺服器不允許CORS,XMLHttpRequest請求實際上是發送出去,并且傳回資料的了,隻是浏覽器沒有讓JS環境拿到而已。
另外,我認為有另外一種資料洩露的可能:黑客可能控制了某個路由,他不能随意抓包,但是他可以在回應頭裡插入一些特别的頭部,比如:
Access-Control-Allow-Credentials: true
那麼,這時XMLHttpRequest請求就是帶cookie的了。
最初的問題
回到最初的問題:
即使XMLHttpRequest是不帶Cookie的,也是有可能造成資料洩露的。比如内部網站是根據IP限制通路的,如果XMLHttpRequest不遵守同源政策,那麼攻擊者可以在使用者浏覽網頁的時候,發起請求,取得内部網站資料。
同域情況下會,不同域情況下不會。如果伺服器設定Access-Control-Allow-Credentials: true ,也是可以跨域帶Cookie的。
會。
總結:
- 禁止手動拼接JSON字元串,一律應當用JSON庫輸出。也不應使用自己實作的ObjectToJson等方法,因為可能有各種沒有考慮到的地方。
- jsonp請求的callback要嚴格過濾,隻允許"_",0到9,a-z, A-Z,即合法的javascript函數的命名。
- jsonp請求也要判斷合法性,比如使用者是否登陸(這點很容易被忽略)。
- 設定好Content-Type(這點對于調試不友善,但是提高了安全性)。
- 以jsonp方式調用第三方的接口,實際相當于引入了第三方的JS代碼,要慎重。
參考:
http://www.json.org/
http://www.slideshare.net/wurbanski/nosql-no-security
https://github.com/douglascrockford/JSON-js
http://toolswebtop.com/ 線上編碼轉換,可以轉換UTF-7
http://www.thespanner.co.uk/2011/05/30/json-hijacking/
http://www.thespanner.co.uk/2009/11/23/bypassing-csp-for-fun-no-profit/
http://stackoverflow.com/questions/1830050/why-same-origin-policy-for-xmlhttprequest