入门客AI创业平台(我带你入门,你带我飞行)
博文笔记

yii2学习之CSRF验证

创建时间:2017-01-06 投稿人: 浏览次数:446

什么是CSRF


CSRF(跨站请求伪造),通过盗用你的身份,发送一些恶意请求,比如更改用户密码、删除账户、发送邮件、以你的身份购买商品等。

攻击原理:用户A访问网站B,登录验证通过后会在用户A的浏览器中产生登录B网站的cookie,这时用户A在没有退出登录情况下访问恶意网站C,C的网站中有去请求网站B的Request,浏览器会带着之前的cookie去请求B,而B无法分别是用户A发出的还是网站C发出的,固恶意网站C就可以模拟用户请求。

如何防止CSRF攻击


目前大多数网站都是采取服务端进行CSRF防御,就是在客户端页面增加伪随机数,服务端返回浏览器信息时setcookie添加相应字段,表单提交数据时增加隐藏字段,该字段根据cookie中的字段,进行md5、base64等处理后以隐藏的hash值post给服务器,然后服务端对表单中的hash值进行验证以确保请求是用户发送的。

攻击者攻击的原理是利用了客户端的COOKIE,但是攻击者是得不到COOKIE具体的内容的,他只是利用。所以攻击者没法在模拟攻击URL中加入token,这样就无法通过验证。

Yii2的CSRF机制


  1. 在yii2工程的environments->index.PHP下添加工程的setCookieValidationKey需要的路径。

    "setCookieValidationKey" => [
            "backend/config/main-local.php",
            "frontend/config/main-local.php",
    ],
    • 1
    • 2
    • 3
    • 4
    • 1
    • 2
    • 3
    • 4

    在执行init时,会调用init.php中setCookieValidationKey函数根据配置的路径生成对应cookieValidationKey 32位随机串。

    $config = [
        "components" => [
            "request" => [
                // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
                "cookieValidationKey" => "nvgfNbUkW3NjixwbQudkQdAm_D6JB9c8",
            ],
        ],
    ];
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    该值会在Response浏览器时将cookie中的value数据通过sha256加密(cookieValidationKey是加密key)后再与value拼接作为新的value通过setcookie传给浏览器缓冲。相应代码如下:

    foreach ($this->getCookies() as $cookie) {
        $value = $cookie->value;
        if ($cookie->expire != 1  && isset($validationKey)) {
           $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
        }
        setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    public function hashData($data, $key, $rawHash = false)
    {
        $hash = hash_hmac($this->macHash, $data, $key, $rawHash);
        if (!$hash) {
            throw new InvalidConfigException("Failed to generate HMAC with hash algorithm: " . $this->macHash);
        }
        return $hash . $data;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这样用户登录的信息就被缓冲到浏览器的cookie中。

  2. 在yii2的Request.php中有两个属性,默认都为true。 
    public $enableCsrfValidation = true; 
    public $enableCookieValidation = true;

    $enableCsrfValidation是否启用CSRF验证,当设置为true时,所有表单等post提交的数据都要经过CSRF验证,如果没有经过验证将返回错误。

    $enableCookieValidation是否对cookie进行验证以防止被更改。

    yii2中csrf验证流程,在form表单提交数据时加上隐藏的input,name是_csrf,值从getCsrfToken获取。 
    <input type="hidden" name="_csrf" value="<?=Yii::$app->request->getCsrfToken() ?>">

    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;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    从上可以看出_csrf的值是通过$token和一个随机生成的$mask经过异或运算后与$mask拼接再经过base64加密后处理的一个字符串,然后看下token是从哪来的,loadCsrfToken函数

    protected function loadCsrfToken()
    {
        if ($this->enableCsrfCookie) {
            return $this->getCookies()->getValue($this->csrfParam);
        } else {
            return Yii::$app->getSession()->get($this->csrfParam);
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    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;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    enableCsrfCookie为true,固token是浏览器cookie中的_csrf字段值。第一次访问时是随机生成的一个32位字段

    综上input中带的隐藏字段值其实就是cookie中的_csrf字段经过某种运算后的值。

    表单提交给服务器后,在Controller.php的beforeAction进行验证

    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;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    public function validateCsrfToken($token = null)
    {
        $method = $this->getMethod();
        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);
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    $trueToken是从cookie中获取的_csrf字段,$token为null,固通过与body中的_csrf字段(即input中提交的隐藏_csrf字段值)或者与head中的HTTP_X_CSRF_TOKEN字段进行比较。

    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;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    将input中的_csrf字段通过base64解码,然后取出前8位的$mask和后面$token然后异或得到真正的$token,用这个$token去和cookie中的token进行比较看是否相同,相同则csrf验证通过。

声明:该文观点仅代表作者本人,入门客AI创业平台信息发布平台仅提供信息存储空间服务,如有疑问请联系rumenke@qq.com。