一.CSRF是什么?
CSRF(Cross-site request forgery),中文名稱:跨站請求偽造,也被稱為:one click attack/session riding,縮寫為:CSRF/XSRF。
CSRF XSS的區別與聯系
XSS 利用的是用戶對指定網站的信任,CSRF 利用的是網站對用戶網頁瀏覽器的信任。
XSS可以作為CSRF的跳板,例如利用XSS在當前網站打開另外一個網站頁面
二.CSRF可以做什么?
你這可以這么理解CSRF攻擊:攻擊者盜用了你的身份,以你的名義發送惡意請求。CSRF能夠做的事情包括:以你名義發送郵件,發消息,盜取你的賬號,甚至於購買商品,虛擬貨幣轉賬......造成的問題包括:個人隱私泄露以及財產安全。
三.CSRF的原理
從上圖可以看出,要完成一次CSRF攻擊,受害者必須依次完成兩個步驟:
1.登錄受信任網站A,並在本地生成Cookie。
2.在不登出A的情況下,訪問危險網站B。
示例1:
銀行網站A,它以GET請求來完成銀行轉賬的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000
危險網站B,它里面有一段HTML的代碼如下:
首先,你登錄了銀行網站A,然后訪問危險網站B,噢,這時你會發現你的銀行賬戶少了1000塊......
為什么會這樣呢?原因是銀行網站A違反了HTTP規范,使用GET請求更新資源。在訪問危險網站B的之前,你已經登錄了銀行網站A,而B中的<img>以GET的方式請求第三方資源(這里的第三方就是指銀行網站了,原本這是一個合法的請求,但這里被不法分子利用了),所以你的瀏覽器會帶上你的銀行網站A的Cookie發出Get請求,去獲取資源“http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,結果銀行網站服務器收到請求后,認為這是一個更新資源操作(轉賬操作),所以就立刻進行轉賬操作......
示例2:
銀行決定把獲取請求數據的方法也改了,改用$_POST,只獲取POST請求的數據,后台處理頁面Transfer.php代碼如下:
<?php session_start(); if (isset($_POST['toBankId'] && isset($_POST['money'])) { buy_stocks($_POST['toBankId'], $_POST['money']); } ?>
然而,危險網站B與時俱進,它改了一下代碼:
<html> <head> <script type="text/javascript"> function steal() { iframe = document.frames["steal"]; iframe.document.Submit("transfer"); } </script> </head> <body onload="steal()"> <iframe name="steal" display="none"> <form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php"> <input type="hidden" name="toBankId" value="11"> <input type="hidden" name="money" value="1000"> </form> </iframe> </body> </html>
如果用戶仍是繼續上面的操作,很不幸,結果將會是再次不見1000塊......因為這里危險網站B暗地里發送了POST請求到銀行!
四.CSRF的防御
1. 升級瀏覽器,最新版本的瀏覽器通常都有同源策略限制.
chrome & firfox測試對比
chrome關閉跨域設置:https://www.jianshu.com/p/d52739cd3392
2. 服務端完善校驗規則,增加風控... 比如給好友轉賬操作,服務端增加轉賬對象是否好友
3. 驗證 HTTP Referer 字段 -- 只能作為輔助
4. 在表單中中添加 token 字段, 服務端驗證
5. 在 HTTP 頭中自定義token屬性,服務端驗證
6. ajax csrf 問題,由於ajax不刷新頁面, 一次性令牌在使用過一次以后, 就無法再使用,二次請求就會失敗
其中3,4的原理是一樣的, 通常實現方式方式的One-Time Tokens (不同的表單包含一個不同的偽隨機值)
在實現One-Time Tokens時,需要注意一點:就是“並行會話的兼容”。如果用戶在一個站點上同時打開了兩個不同的表單,CSRF保護措施不應該影響到他對任何表單的提交。考慮一下如果每次表單被裝入時站點生成一個偽隨機值來覆蓋以前的偽隨機值將會發生什么情況:用戶只能成功地提交他最后打開的表單,因為所有其他的表單都含有非法的偽隨機值。
做了以上步驟,是不是就安全了呢?
答案是否,由於token存在於cookie,表單隱藏域,header頭中, 結合xss攻擊,攻擊者是可以獲取的網站token的, 然后遠程發送給csrf站點,csrf站點就可以使用獲取到的token進行攻擊了
五. YII2 csrf的實現
驗證位置在yii\web\controller::beforeAction中, 所以在子控制器里, 如果重寫beforeAction一定要調用一下parent::beforeAction, 其他類似的還有init, afterAction...
/** * @inheritdoc */ public function beforeAction($action) { if (parent::beforeAction($action)) { if ($this->enableCsrfValidation && Yii::$app->getErrorHandler()->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) { throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.')); } return true; } else { return false; } }
yii\web\request中csrf的實現
/** * Returns the token used to perform CSRF validation. * * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/). * This token may be passed along via a hidden field of an HTML form or an HTTP header value * to support CSRF validation. * @param boolean $regenerate whether to regenerate CSRF token. When this parameter is true, each time * this method is called, a new CSRF token will be generated and persisted (in session or cookie). * @return string the token used to perform CSRF validation. */ public function getCsrfToken($regenerate = false) { if ($this->_csrfToken === null || $regenerate) { if ($regenerate || ($token = $this->loadCsrfToken()) === null) { $token = $this->generateCsrfToken(); } // the mask doesn't need to be very random $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; $mask = substr(str_shuffle(str_repeat($chars, 5)), 0, static::CSRF_MASK_LENGTH); // The + sign may be decoded as blank space later, which will fail the validation $this->_csrfToken = str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask))); } return $this->_csrfToken; } /** * Loads the CSRF token from cookie or session. * @return string the CSRF token loaded from cookie or session. Null is returned if the cookie or session * does not have CSRF token. */ protected function loadCsrfToken() { if ($this->enableCsrfCookie) { return $this->getCookies()->getValue($this->csrfParam); } else { return Yii::$app->getSession()->get($this->csrfParam); } } /** * Generates an unmasked random token used to perform CSRF validation. * @return string the random token for CSRF validation. */ protected function generateCsrfToken() { $token = Yii::$app->getSecurity()->generateRandomString(); if ($this->enableCsrfCookie) { $cookie = $this->createCsrfCookie($token); Yii::$app->getResponse()->getCookies()->add($cookie); } else { Yii::$app->getSession()->set($this->csrfParam, $token); } return $token; } /** * Returns the XOR result of two strings. * If the two strings are of different lengths, the shorter one will be padded to the length of the longer one. * @param string $token1 * @param string $token2 * @return string the XOR result */ private function xorTokens($token1, $token2) { $n1 = StringHelper::byteLength($token1); $n2 = StringHelper::byteLength($token2); if ($n1 > $n2) { $token2 = str_pad($token2, $n1, $token2); } elseif ($n1 < $n2) { $token1 = str_pad($token1, $n2, $n1 === 0 ? ' ' : $token1); } return $token1 ^ $token2; } /** * @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. */ public function getCsrfTokenFromHeader() { $key = 'HTTP_' . str_replace('-', '_', strtoupper(static::CSRF_HEADER)); return isset($_SERVER[$key]) ? $_SERVER[$key] : null; } /** * Creates a cookie with a randomly generated CSRF token. * Initial values specified in [[csrfCookie]] will be applied to the generated cookie. * @param string $token the CSRF token * @return Cookie the generated cookie * @see enableCsrfValidation */ protected function createCsrfCookie($token) { $options = $this->csrfCookie; $options['name'] = $this->csrfParam; $options['value'] = $token; return new Cookie($options); } /** * Performs the CSRF validation. * * This method will validate the user-provided CSRF token by comparing it with the one stored in cookie or session. * This method is mainly called in [[Controller::beforeAction()]]. * * Note that the method will NOT perform CSRF validation if [[enableCsrfValidation]] is false or the HTTP method * is among GET, HEAD or OPTIONS. * * @param string $token the user-provided CSRF token to be validated. If null, the token will be retrieved from * the [[csrfParam]] POST field or HTTP header. * This parameter is available since version 2.0.4. * @return boolean whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true. */ public function validateCsrfToken($token = null) { $method = $this->getMethod(); // only validate CSRF token on non-"safe" methods http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) { return true; } $trueToken = $this->loadCsrfToken(); if ($token !== null) { return $this->validateCsrfTokenInternal($token, $trueToken); } else { return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken) || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken); } } /** * Validates CSRF token * * @param string $token * @param string $trueToken * @return boolean */ private function validateCsrfTokenInternal($token, $trueToken) { $token = base64_decode(str_replace('.', '+', $token)); $n = StringHelper::byteLength($token); if ($n <= static::CSRF_MASK_LENGTH) { return false; } $mask = StringHelper::byteSubstr($token, 0, static::CSRF_MASK_LENGTH); $token = StringHelper::byteSubstr($token, static::CSRF_MASK_LENGTH, $n - static::CSRF_MASK_LENGTH); $token = $this->xorTokens($mask, $token); return $token === $trueToken; }
上面的代碼中可以看出,真實的token是存在session或者cookie中的,除非強制重新生成, 否則在失效之前這個token一直不會變化.
頁面上每次看到的token不一樣, 是因為在generateCsrfToken中給token增加了混淆的mast, 這個機制導致yii2的csrf token不是使用一次就失效的
這種方式的危險性要比一次性令牌方式大
再來看下YII2 ajax對csrf的支持, 要想在ajax中無感使用csrf, 就必須引用yii.js, 同時頁面渲染要輸出一個含有csrf信息的mata
可以看到initCsrfHandle給ajax請求增加了X-CSER-Token頭,X-CSER-Token的值是從一個name=csrf-token的mata中獲取的
而在服務端的驗證函數validateCsrfToken中對應的驗證邏輯
看到這里就清楚了為嘛yii2的ajax post請求每次都能通過驗證,因為上面提到了:yii2的csrf token不是使用一次就失效的
function initCsrfHandler() { // automatically send CSRF token for all AJAX requests $.ajaxPrefilter(function (options, originalOptions, xhr) { if (!options.crossDomain && pub.getCsrfParam()) { xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); } }); pub.refreshCsrfToken(); }
//getCsrfToken的定義
getCsrfToken: function () { return $('meta[name=csrf-token]').attr('content'); },