什么是CSRF攻擊
跨站請(qǐng)求偽造(英語(yǔ):Cross-site request forgery)燕锥,也被稱為 one-click attack 或者 session riding桑腮,通常縮寫為 CSRF 或者 XSRF狸演, 是一種挾制用戶在當(dāng)前已登錄的Web應(yīng)用程序上執(zhí)行非本意的操作的攻擊方法匹涮。跟跨網(wǎng)站腳本(XSS)相比贴硫,XSS 利用的是用戶對(duì)指定網(wǎng)站的信任炮障,CSRF 利用的是網(wǎng)站對(duì)用戶網(wǎng)頁(yè)瀏覽器的信任目派。
防御措施
檢查Referer字段
HTTP頭中有一個(gè)Referer字段,這個(gè)字段用以標(biāo)明請(qǐng)求來(lái)源于哪個(gè)地址胁赢。在處理敏感數(shù)據(jù)請(qǐng)求時(shí)企蹭,通常來(lái)說,Referer字段應(yīng)和請(qǐng)求的地址位于同一域名下智末。以上文銀行操作為例谅摄,Referer字段地址通常應(yīng)該是轉(zhuǎn)賬按鈕所在的網(wǎng)頁(yè)地址,應(yīng)該也位于www.examplebank.com之下系馆。而如果是CSRF攻擊傳來(lái)的請(qǐng)求送漠,Referer字段會(huì)是包含惡意網(wǎng)址的地址,不會(huì)位于www.examplebank.com之下由蘑,這時(shí)候服務(wù)器就能識(shí)別出惡意的訪問闽寡。
這種辦法簡(jiǎn)單易行,工作量低尼酿,僅需要在關(guān)鍵訪問處增加一步校驗(yàn)爷狈。但這種辦法也有其局限性,因其完全依賴瀏覽器發(fā)送正確的Referer字段裳擎。雖然http協(xié)議對(duì)此字段的內(nèi)容有明確的規(guī)定涎永,但并無(wú)法保證來(lái)訪的瀏覽器的具體實(shí)現(xiàn),亦無(wú)法保證瀏覽器沒有安全漏洞影響到此字段鹿响。并且也存在攻擊者攻擊某些瀏覽器羡微,篡改其Referer字段的可能。
添加校驗(yàn)token
由于CSRF的本質(zhì)在于攻擊者欺騙用戶去訪問自己設(shè)置的地址抢野,所以如果要求在訪問敏感數(shù)據(jù)請(qǐng)求時(shí)拷淘,要求用戶瀏覽器提供不保存在cookie中,并且攻擊者無(wú)法偽造的數(shù)據(jù)作為校驗(yàn)指孤,那么攻擊者就無(wú)法再運(yùn)行CSRF攻擊启涯。這種數(shù)據(jù)通常是窗體中的一個(gè)數(shù)據(jù)項(xiàng)。服務(wù)器將其生成并附加在窗體中恃轩,其內(nèi)容是一個(gè)偽隨機(jī)數(shù)结洼。當(dāng)客戶端通過窗體提交請(qǐng)求時(shí),這個(gè)偽隨機(jī)數(shù)也一并提交上去以供校驗(yàn)叉跛。正常的訪問時(shí)松忍,客戶端瀏覽器能夠正確得到并傳回這個(gè)偽隨機(jī)數(shù),而通過CSRF傳來(lái)的欺騙性攻擊中筷厘,攻擊者無(wú)從事先得知這個(gè)偽隨機(jī)數(shù)的值鸣峭,服務(wù)端就會(huì)因?yàn)樾r?yàn)token的值為空或者錯(cuò)誤宏所,拒絕這個(gè)可疑請(qǐng)求。
Yii2中csrf的實(shí)現(xiàn)原理
Yii2是使用token來(lái)預(yù)防csrf攻擊的摊溶。
在配置文件中開啟
'request' => [
'enableCsrfValidation' => true,
],
校驗(yàn)token
在所有控制器繼承的controller基類中爬骤,有個(gè)請(qǐng)求回調(diào)函數(shù)在請(qǐng)求處理前驗(yàn)證token。
在下面代碼中莫换,先判斷是否開啟了csrf驗(yàn)證霞玄,在判斷目前異常處理器是否捕獲了異常,最后才校驗(yàn)token
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;
}
return false;
}
接下來(lái)看看validateCsrfToken函數(shù)
public function validateCsrfToken($clientSuppliedToken = null)
{
// 先判斷http請(qǐng)求方法拉岁,如果是get坷剧、head、options方法則直接跳過喊暖,不認(rèn)證惫企。
$method = $this->getMethod();
// only validate CSRF token on non-"safe" methods https://tools.ietf.org/html/rfc2616#section-9.1.1
if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
return true;
}
// 獲取服務(wù)器原始的token
$trueToken = $this->getCsrfToken();
// 如果已經(jīng)拿到了瀏覽器帶過來(lái)的token,則直接與上面的服務(wù)器端原始的token進(jìn)行比較
if ($clientSuppliedToken !== null) {
return $this->validateCsrfTokenInternal($clientSuppliedToken, $trueToken);
}
// 否則從請(qǐng)求body或者請(qǐng)求head中獲取瀏覽器端的token陵叽,再進(jìn)行比較
return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken)
|| $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
}
先看看服務(wù)器端原始的token是怎么來(lái)的.
從注釋中可以看到雅任,服務(wù)端生成的原始token一般是保存在session或者cookie中。然后瀏覽器會(huì)通過html表單的隱藏字段
或者h(yuǎn)ttp的header帶給服務(wù)器咨跌,然后兩者比較進(jìn)行驗(yàn)證。
/**
* Returns the token used to perform CSRF validation.
*
* This token is generated in a way to prevent [BREACH attacks](http://breachattack.com/). It may be passed
* along via a hidden field of an HTML form or an HTTP header value to support CSRF validation.
* @param bool $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)
{
// 因?yàn)檫@個(gè)函數(shù)在一次請(qǐng)求中會(huì)多次調(diào)用硼婿,所以會(huì)保存token在_csrfToken字段中
if ($this->_csrfToken === null || $regenerate) {
$token = $this->loadCsrfToken();
if ($regenerate || empty($token)) {
// 重新生成token
$token = $this->generateCsrfToken();
}
// 對(duì)token進(jìn)行簡(jiǎn)單加密
$this->_csrfToken = Yii::$app->security->maskToken($token);
}
return $this->_csrfToken;
}
看看loadCsrfToken函數(shù)锌半,即從cookie或者session中獲取服務(wù)端的token
/**
* 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()
{
// 默認(rèn)是保存在cookie中的,比較不安全
if ($this->enableCsrfCookie) {
return $this->getCookies()->getValue($this->csrfParam);
}
return Yii::$app->getSession()->get($this->csrfParam);
}
那么token是怎么生成的呢寇漫?看下面代碼可知token就是一個(gè)隨機(jī)字符串[A-Za-z0-9_-]刊殉,生成后保存在cookie中或者session中
/**
* 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;
}
校驗(yàn)函數(shù)則比較簡(jiǎn)單了,就是解密后進(jìn)行比較
private function validateCsrfTokenInternal($clientSuppliedToken, $trueToken)
{
if (!is_string($clientSuppliedToken)) {
return false;
}
$security = Yii::$app->security;
return $security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken));
}
最后看看加解密函數(shù)
/**
* Masks a token to make it uncompressible.
* Applies a random mask to the token and prepends the mask used to the result making the string always unique.
* Used to mitigate BREACH attack by randomizing how token is outputted on each request.
* @param string $token An unmasked token.
* @return string A masked token.
* @since 2.0.12
*/
public function maskToken($token)
{
// The number of bytes in a mask is always equal to the number of bytes in a token.
$mask = $this->generateRandomKey(StringHelper::byteLength($token));
return StringHelper::base64UrlEncode($mask . ($mask ^ $token));
}
/**
* Unmasks a token previously masked by `maskToken`.
* @param string $maskedToken A masked token.
* @return string An unmasked token, or an empty string in case of token format is invalid.
* @since 2.0.12
*/
public function unmaskToken($maskedToken)
{
$decoded = StringHelper::base64UrlDecode($maskedToken);
$length = StringHelper::byteLength($decoded) / 2;
// Check if the masked token has an even length.
if (!is_int($length)) {
return '';
}
return StringHelper::byteSubstr($decoded, $length, $length) ^ StringHelper::byteSubstr($decoded, 0, $length);
}
值得一提的是州胳,存在cookie中也是比較不安全的记焊,但是要比存在session中高效?栓撞?貌似這兩種安全性差不多
最后遍膜,查看getCsrfToken函數(shù)的調(diào)用,發(fā)現(xiàn)有多次調(diào)用瓤湘,比如登錄的時(shí)候調(diào)用并且刷新token瓢颅,生成表單的時(shí)候回調(diào)用。
總結(jié)
防止csrf攻擊的主要核心在于如何確認(rèn)該請(qǐng)求是否是自己方的代碼產(chǎn)生的弛说,而不是別人惡意代碼偽造的挽懦。
token相當(dāng)于一個(gè)身份的象征,一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)token木人,從而確保請(qǐng)求的身份的認(rèn)定信柿。