什么是CSRF攻击
百度百科
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
防御措施
检查Referer字段
HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。
添加校验token
由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。
Yii2中csrf的实现原理
Yii2是使用token来预防csrf攻击的。
在配置文件中开启
'request' => [
'enableCsrfValidation' => true,
],
校验token
在所有控制器继承的controller基类中,有个请求回调函数在请求处理前验证token。
在下面代码中,先判断是否开启了csrf验证,在判断目前异常处理器是否捕获了异常,最后才校验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;
}
接下来看看validateCsrfToken函数
public function validateCsrfToken($clientSuppliedToken = null)
{
// 先判断http请求方法,如果是get、head、options方法则直接跳过,不认证。
$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;
}
// 获取服务器原始的token
$trueToken = $this->getCsrfToken();
// 如果已经拿到了浏览器带过来的token,则直接与上面的服务器端原始的token进行比较
if ($clientSuppliedToken !== null) {
return $this->validateCsrfTokenInternal($clientSuppliedToken, $trueToken);
}
// 否则从请求body或者请求head中获取浏览器端的token,再进行比较
return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken)
|| $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
}
先看看服务器端原始的token是怎么来的.
从注释中可以看到,服务端生成的原始token一般是保存在session或者cookie中。然后浏览器会通过html表单的隐藏字段
或者http的header带给服务器,然后两者比较进行验证。
/**
* 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)
{
// 因为这个函数在一次请求中会多次调用,所以会保存token在_csrfToken字段中
if ($this->_csrfToken === null || $regenerate) {
$token = $this->loadCsrfToken();
if ($regenerate || empty($token)) {
// 重新生成token
$token = $this->generateCsrfToken();
}
// 对token进行简单加密
$this->_csrfToken = Yii::$app->security->maskToken($token);
}
return $this->_csrfToken;
}
看看loadCsrfToken函数,即从cookie或者session中获取服务端的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()
{
// 默认是保存在cookie中的,比较不安全
if ($this->enableCsrfCookie) {
return $this->getCookies()->getValue($this->csrfParam);
}
return Yii::$app->getSession()->get($this->csrfParam);
}
那么token是怎么生成的呢?看下面代码可知token就是一个随机字符串[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;
}
校验函数则比较简单了,就是解密后进行比较
private function validateCsrfTokenInternal($clientSuppliedToken, $trueToken)
{
if (!is_string($clientSuppliedToken)) {
return false;
}
$security = Yii::$app->security;
return $security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken));
}
最后看看加解密函数
/**
* 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函数的调用,发现有多次调用,比如登录的时候调用并且刷新token,生成表单的时候回调用。
总结
防止csrf攻击的主要核心在于如何确认该请求是否是自己方的代码产生的,而不是别人恶意代码伪造的。
token相当于一个身份的象征,一个请求对应一个token,从而确保请求的身份的认定。