原理
CSRF,Cross Site Request Forgery,即跨站點請求偽造。
這種攻擊是指,在用戶正常登錄系統以后,攻擊者誘使用戶訪問一些非法鏈接,以執行一些非法操作。比如:如果刪除用戶操作(如,yourdomain.com/deluser?id=123)沒有經過防范CSRF的處理,那么,假設用戶登錄系統后,攻擊者誘使用戶同時訪問了攻擊者的站點的一個鏈接(該鏈接正好為yourdomain.com/deluser?id=123),那么,系統就會在用戶不知情的情況下丟失一個用戶。
在這個例子中,跨站請求中的鏈接之所以被正常執行,首先是因為請求中瀏覽器正常發送了yourdomain的認證信息(一般保存在cookie中),服務器根本不知道該請求是用戶為之,還是惡意為之。其次,就是請求中的參數是可以被猜測的。這兩個條件,構成了CSRF攻擊的全部條件。
這里還需要強調一下,如果認證基於cookie,那么實際上還有第三個條件:如果cookie是本地cookie,瀏覽器還需要允許跨域發送本地cookie,即如果請求是第三方網站發起的,應帶上請求的域的cookie。IE默認是不允許跨域發送本地cookie的(session cookie則無此限制),而firefoxe就默認允許的。
原理如圖:
應對策略
1:采用token的形式。
采用token是指讓請求所帶的參數變的不可猜測。即,每次需要保護的請求都要帶上一個額外的參數,該參數可以是sessionid(一定要是額外的參數,但是其值可以為sessionid),也可以是另外的無法被猜測的一個值。然后,服務器在得到這個請求后,再驗證該值是否匹配。
可能有人會進一步提出,不是sessionid也是可以非法得到的嗎,或者說用戶的sessionid是沒有授權被操作的?答案是沒錯,但是,那又是另外的攻擊方法(涉及到會話劫持和權限欺騙)了,在這里,僅僅防御的是CSRF攻擊。
不過為了保險起見,我們可以用sessionid+salt,然后散列的方式來生成這個token。
采用token的形式,我們還需要考慮該token,也就是客戶端所帶的這個參數的保存問題。從CSRF的本質考慮,token的保存首先不能保存在cookie中,因為cookie本身就是在發送請求的時候可以被帶上。
其次,token可以保存在服務器端嗎,如,我們可以為當前請求設定一個唯一標識,然后保存在session中。答案當然也是不行的,我們可以假設完成一次請求包含兩個部分:發起請求的URL(或程序),處理請求的URL(或程序),誠然,這種方式我們防住了單獨請求”處理請求的URL”的CSRF攻擊。但是,既然攻擊者得到了處理請求的頁面,那么,他在偽造CSRF的時候,只要帶上了發送請求的頁面,就依然可以完成一次攻擊。
所以,token的保存只能是保存在發送到客戶端的頁面中,然后客戶端在接下來發送的請求的時候,帶上這個參數就可以了。當然,如果頁面本身已經被XSS攻破,那么攻擊者仍舊可以偽造一次合法請求,但這已經不是防范CSRF的范疇了,而是防范XSS。
2:每次需要被保護的請求發送時,都要求用戶輸入密碼;
3:每次需要被保護的請求發送時,都帶上referrer。不過這並不是應對的最佳策略,因為referrer是可以被輕易偽造的。
具體措施
以下具體措施針對token的形式。
n 遍歷前台所有發送請求的地方
1:文件查找前台所有的”svc”,”ajax”,”.aspx”,”.html”,”.htm”
2:文件查找前台所有的”form”
根據以上的查找,匯總到如下的表格:
序號 |
文件 |
代碼行 |
GET/POST |
處理完成否 |
|
|
|
|
|
n 處理請求
篩選出需要進行CSRF處理的請求。然后對請求做如下處理:
如果是GET方式發送的請求,則為請求加入參數token=[value],其中[value]為sessionid的值;
如果是POST方式發送的請求,則為Form加入隱藏的input,其name為token,其值為sessionid。
n 遍歷所有的請求處理處
1:遍歷所有的svc,為svc的方法增加token參數
2:遍歷所有的aspx頁面的code-behind
3:遍歷所有其它的后台方法,如果存在的話,如控制器方法(在EL中並不存在)。
根據以上的查找,匯總到如下的表格
序號 |
文件 |
代碼行 |
處理完成否 |
|
|
|
|
n 處理請求處理處
處理參數中的token,檢測該token是否存在於當前的sessionid中,如果存在,則放行,否則異常;
以上全部的邏輯用代碼表示,大致如下:
protected void Page_Load(object sender, EventArgs e) { string token = CreateToken(); PutTokenToClient(token); SaveTokenInServer(token); } protected void ButtonDosomething_Click(object sender, EventArgs e) { string token = GetTokenFromRequest(); //需要csrf保護的地方就check才放行 if (TokenIsOK(token)) { //todo: go } else { //todo: block } } private string GetTokenFromRequest() { //todo 從請求中得到coken,一般為URL QueryString或表單元素 throw new NotImplementedException(); } private void PutTokenToClient(string token) { //todo 將其保存到前台,如請求的url,或隱藏的input } private void SaveTokenInServer(string token) { //一般保存在session中 Session["CRSFToken"] = token; } private bool TokenIsOK(string token) { string tokenInServer = Session["CRSFToken"].ToString(); return tokenInServer == token ? true : false; } public string _salt = "asdfkl@,.;#sss13131313"; public string CreateToken() { return MD5(Session.SessionID + _salt); } private void ClearToken() { Session["CRSFToken"] = string.Empty; } private string MD5(string p) { throw new NotImplementedException(); }