Yii中的csrf

什么是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,从而确保请求的身份的认定。

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?