上一篇我們說了用腳本注入去公雞小熊哥的博文評論。普通的腳本注入很容易就被前端后臺全方位的腳本過濾給處理掉,完全沒有殺傷力。一般來說遇到<, > , =" 等等可能加載執行腳本的用戶輸入,轉換為< ; & gt ; 等等即可作為數據打印到 DOM 中,而不是作為腳本執行。詳細情況參考上一篇文章 。我敢保證,@Swanky 肯定是受了我的蠱惑去 POST 了 三萬多條評論導致被 @簡書首席封號官 封號了。
偽造請求的過程
這次來說說偽造公雞(Cross Site Request Forgery)的事情。通俗點講就是 有人給你發送了一個鏈接,然后你手賤點開,是看起來很正常的網頁(A.com/index.html),結果暗中被發送了一個指向 招商銀行轉賬服務(B.com/transfer) 的POST請求,這個請求是攻擊者在A.com/index.html 腳本中偽造的,因此請求的發送方屬于A.com域,而不屬于B.com/transfer 的域, 所以叫做 跨站偽造請求
// attack.html. 為了在這個偽造的網頁中自動發送請求并且帶上 受害用戶的cookie,
// 一般都會在hidden iframe中加載一份正規網站的頁面,而且攻擊表單也是隱藏的。
// 表面上這個偽造網頁是很正規的,并無異常。
<form id="thisForm" method="POST" target="_blank" action="http://www.bank.com/transfer">
<input type="text" name="name" value="csrf_Attack2!!!">
</form>
<iframe name="tab" src="http://www.bank.com" style="display:none;">
</iframe>
XSRF 原理
XSRF的關鍵就是 假如正規網站(B.com)的服務器端沒有防御機制,只是對于發來的請求做用戶身份驗證,那么偽造請求就可以借用受害者瀏覽器自動發送的cookie蒙混過關,實行惡意攻擊。其中有幾個問題需要明確:
- 用戶身份信息一般通過Http Request 的頭部cookie 字段的值來傳遞,cookie中往往包含sessionid 這樣的用戶會話信息,服務器通過比對服務端內存或數據庫(Redis等)中維持的session就可以知道是哪個用戶在發請求,Session有無過期。 這一過程一般不驗證請求發送方的 域。
- 跨站偽造請求成功的前提是 B.com 提供了可供跨域調用的服務。因為我們知道大部分瀏覽器和服務器都禁止腳本請求跨域資源,這種行為一般不太安全。但是我非得請求呢?? 所以除了用JSONP 來GET跨域資源,更有采用CORS(Cross-origin resource sharing,跨域資源共享是W3C標準)來實現POST,PUT請求跨域的。 CORS的詳細介紹可以參考這里.
通常情況下,假如服務器端開啟CORS配置允許任何來源的請求,就很難分辨接收到的請求是用戶自主發送的(往往是和B.com同一個域下的網頁),還是攻擊者冒充用戶身份偽造的攻擊請求(往往不同域,例如A.com)。
XSRF防御——Token
經過上面的介紹,其實防御的辦法大概都可以推斷出來了。關鍵就是區分來自于各種域的請求,哪些是合法用戶發送的,哪些是被借用了身份發送來的。
- 配置服務器的CORS,只允許白名單的域發來請求,這樣最直接。只要白名單的網站不被攻擊注入惡意腳本就基本安全了。以nodejs和express為例,配置如下:
app.all('*', function(req, res, next) {
// 如果用"*", 表示接受任何域發來的請求, 這里只配置一個域名
res.header("Access-Control-Allow-Origin", "www.icbc.com");
res.header("Access-Control-Allow-Headers", "X-Requested-With, accept, origin, content-type");
res.header("Access-Control-Allow-Methods", "PUT,POST,OPTIONS");
next();
}
2.bank.com 的服務器端和客戶端通力合作,協商一個除了Session之外的二次身份驗證——Token。
Token驗證的原理是:每次用戶登陸成功,就根據SessionID和特定算法生成一個Token,發回客戶端(動態添加到一個隱藏域中,例如<input> 或者JS變量中),合法客戶端在隨后的請求中加入Token作為數據體,服務器端每次接受請求都要去驗證SessionID 和 Token,雙保險。以nodejs為例,一休哥自制服務端關鍵代碼:
// 當用戶登錄驗證成功時,隨即生成與該用戶Session綁定的Token
req.session.key = (Math.random()* Math.pow(10, 17));
var crsfToken = generateToken(parseInt(req.session.key).toString());
// 返回token給客戶端,客戶端將token動態添加至DOM或者變量中
res.send({"status": returnData, "refreshpage": redirectpage, "token": crsfToken});
// return Hex string as token.
// 加密算法遠比這個復雜。這里只是演示,這樣也很難猜出token
function generateToken(sess) {
var tk = "", a = 0.834415, b = 1.214414, c = 1.83121;
for(var i=0; i < sess.length; i++){
var baseNum = parseInt(sess[i]);
tk += (a * Math.pow(baseNum, b) + c).toFixed(0);
}
var tkNum = parseInt(tk);
return tkNum.toString(16);
}
// 當收到請求后,除了驗證session,再根據session計算一次token,比對客戶端發來的是否一致。
// check Token to prevent XSRF...
function checkToken(session, params) {
var validToken = generateToken(session.name);
if (params.token && params.token == validToken) {
return true;
} else {
console.error("CSRF Token not valid! Attack Found !!");
return false;
}
}
再補充一點,因為冒充身份的請求一般都是趁受害者處于登錄狀態了,所以DOM中沒有Token,而經過合法登錄后才會有Token存在。這樣就較好地防御了跨站偽造請求攻擊。
寫在最后
現如今,網絡安全形勢越來越嚴峻,作為一個Web前端工程師,之前很少注意這一點。至此已經更新了兩篇 跟前端工程師相關的網絡安全 筆記了。希望大家喜歡本文,有所收獲。
傳送門:注意安全第一篇 XSS 跨站腳本注入
另外一篇很棒的英文參考 CSRF Attacks, XSRF or Sea-Surf – What They Are and How to Defend Against Them