【Yii2】yii2學習之CSRF驗證

什麼是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機制

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

'setCookieValidationKey' => [
        'backend/config/main-local.php',
        'frontend/config/main-local.php',
],

在執行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',
        ],
    ],
];

該值會在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);
}
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;
}

這樣用戶登錄的信息就被緩衝到瀏覽器的cookie中。

在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;
}

從上可以看出_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);
    }
}
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;
}

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;
    }
}
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);
    }
}

$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;
}

將input中的_csrf字段通過base64解碼,然後取出前8位的$mask和後面$token然後異或得到真正的$token,用這個$token去和cookie中的token進行比較看是否相同,相同則csrf驗證通過。

轉自:http://blog.csdn.net/xiaog351/article/details/47446687

發佈了288 篇原創文章 · 獲贊 964 · 訪問量 141萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章