yii2的csrf验证原理分析及token缓存解决方案
本文主要分三个部分,首先简单介绍csrf,接着对照源码重点分析一下yii框架的验证原理,最后针对页面缓存导致的token被缓存提出一种可行的方案。涉及的知识点会作为附录附于文末。
1.csrf描述
csrf全称为“cross-site request forgery”,是在用户合法的session内发起的攻击。黑客通过在网页中嵌入web恶意请求代码,并诱使受害者访问该页面,当页面被访问后,请求在受害者不知情的情况下以受害者的合法身份发起,并执行黑客所期待的动作。以下html代码提供了一个“删除产品”的功能:
<a href="http://www.shop.com/delproducts.php?id=100" "javascript:return confirm('are you sure?')">delete</a>
假设程序员在后台没有对该“删除产品”请求做相应的合法性验证,只要用户访问了该链接,相应的产品即被删除,那么黑客可通过欺骗受害者访问带有以下恶意代码的网页,即可在受害者不知情的情况下删除相应的产品。
2.yii的csrf验证原理 /vendor/yiisoft/yii2/web/request.php简写为request.php
/vendor/yiisoft/yii2/web/controller.php简写为controller.php
开启csrf验证
在控制器里将enablecsrfvalidation为true,则控制器内所有操作都会开启验证,通常做法是将enablecsrfvalidation为false,而将一些敏感操作设为true,开启局部验证。
public $enablecsrfvalidation = false; /** * @param \yii\base\action $action * @return bool * @desc: 局部开启csrf验证(重要的表单提交必须加入验证,加入$accessactions即可 */ public function beforeaction($action){ $currentaction = $action->id; $accessactions = ['vote','like','delete','download']; if(in_array($currentaction,$accessactions)) { $action->controller->enablecsrfvalidation = true; } parent::beforeaction($action); return true; }
生成token字段
在request.php
首先通过安全组件security获取一个32位的随机字符串,并存入cookie或session,这是原生的token.
/** * 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; }
接着通过一系列加密替换操作,生成加密后_csrftoken,这个是传给浏览器的token. 先随机产生csrf_mask_length(yii2里默认是8位)长度的字符串 mask
对mask和token进行如下运算 str_replace('+', '.', base64_encode($mask . $this->xortokens($token, $mask))); $this->xortokens($arg1,$arg2)
是一个先补位异或运算
/** * 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; } 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; }
验证token
在controller.php里调用request.php里的validatecsrftoken方法
/** * @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; } return false; } public function validatecsrftoken($token = null) { $method = $this->getmethod(); if (!$this->enablecsrfvalidation || in_array($method, ['get', 'head', 'options'], true)) { return true; } $truetoken = $this->loadcsrftoken();//如果开启了enablecsrfcookie,csrftoken就从cookie里取,否者从session里取(更安全) if ($token !== null) { return $this->validatecsrftokeninternal($token, $truetoken); } else { return $this->validatecsrftokeninternal($this->getbodyparam($this->csrfparam), $truetoken) || $this->validatecsrftokeninternal($this->getcsrftokenfromheader(), $truetoken); } }
获取客户端传入
$this->getbodyparam($this->csrfparam)
然后是validatecsrftokeninternal
private function validatecsrftokeninternal($token, $truetoken) { if (!is_string($token)) { return false; } $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; }
加密时用的是 str_replace('+', '.', base64_encode(mask.mask.this->xortokens(token,token,mask)));
解密 1.首先要把.替换成+ 2.然后base64_decode 再 根据长度分别取出mask和mask和this->xortokens(token,token,mask) ; 为了说明方便 this−>xortokens(this−>xortokens(token, $mask) 这里称作 token1 然后 进行mask和token1的异或运算,即得token 注意在加密时
token1=token^mask
所以 解密时
token=mask^token1=mask^(token^mask)
3.token缓存的解决方案
当页面整体被缓存后,token也被缓存导致验证失败,一种常见的解决思路是每次提交前重新获取token,这样就可以通过验证了。
附录:
str_pad()
,该函数返回 input 被从左端、右端或者同时两端被填充到制定长度后的结果。如果可选的 pad_string 参数没有被指定,input 将被空格字符填充,否则它将被 pad_string 填充到指定长度;
str_shuffle()
函数打乱一个字符串,使用任何一种可能的排序方案。
因为yii2 csrf的验证的加解密 涉及到异或运算
所以需要先补充php里字符串异或运算的相关知识,不需要的可以跳过
^异或运算 不一样返回1 否者返回 0 在php语言中,经常用来做加密的运算,解密也直接用^就行 字符串运算时 利用字符的ascii码转换为2进制来运算 单个字符运算
1.对于单个字符和单个字符的 直接计算其结果即可 比如表里的a^b
2.对于长度一样的多个字符串 如表里的ab^cd 计算a^c对应的结果和和b^d对应的结果 对应的字符连接起来