1. 程式人生 > >yii2框架-yii2的防禦csrf攻擊機制(十六)

yii2框架-yii2的防禦csrf攻擊機制(十六)

前段時間工作比較忙,好幾天沒有時間寫一下關於yii2框架的知識總結了。因為上一次需要實現一些功能防禦csrf的這種攻擊,所以整理一下這方面的知識點。
對於什麼是csrf,中文名稱:跨站請求偽造,可以在百度上搜索資料,詳細瞭解這一方面的概念。對於我們是非常有幫助的。
yii2的csrf的實現功能是在yii\web\request類實現功能的。
request類中的屬性,預設是true的。
public $enableCsrfValidation = true;
所以我們在配置檔案中的request元件中可以配置該值
request => [
'enableCookieValidation' => true,
]
這是全域性有效的,也就是說每一個post的請求,都會啟用csrf的防禦攻擊的功能,即進行驗證。
簡單的說,整個訪問策略如下:

(1)通過Yii::$app->request->csrfToken 第一次訪問獲取csrfToken時,直接到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;
}
每一次訪問$this->_csrfToken都會等於null,$regenerate 預設等於false。所以接著執行$token = $this->loadCsrfToken()這個函式。
protected function loadCsrfToken()
    {
        if ($this->enableCsrfCookie) {
            return $this->getCookies()->getValue($this->csrfParam);
        } else {
            return Yii::$app->getSession()->get($this->csrfParam);
        }
}
去cookie中獲取$_COOKIE['_csrf']這個token,由於第一次訪問這個token肯定不會存在,故返回null。所以就會去執行$this->generateCsrfToken()。
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;
}
這個函式就是隨意建立一個token字串,然後將它儲存在$_COOKIE['_csrf']中。這樣子在網站的根目錄/,COOKIE就存在了這個token了,並且返回這個token。只要我們沒有關閉整個網頁,那麼這個$_COOKIE['_csrf']的值就不會變,也就代表本機客戶端的唯一憑證。
接著再看一下getCsrfToken()函式裡的這幾行程式碼:
$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)));
這裡是利用token和字串,通過64位進行編碼加密生成_csrfToken並且返回,也就是獲取csrfToken這個值了。
(2)第二次訪問時,Yii::$app->request->csrfToken,由於$token = $this->loadCsrfToken()這個函式訪問已經可以獲取到token,也就是獲取網站的$_COOKIE['_csrf']的值,所以不會再次重新生成的,所以接著進行64位編碼加密生成_csrfToken並且返回。
(3)那麼我們需要將資料post過去的時候,我們會在yii\web\conreoller的類中的beforeAction($action)函式進行驗證
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;
}
通過Yii::$app->getRequest()->validateCsrfToken()這個函式驗證
public function validateCsrfToken($token = null)
    {
        $method = $this->getMethod();

        // only validate CSRF token on non-"safe" methods http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
        if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
            return true;
        }
        $trueToken = $this->loadCsrfToken();

        var_dump($trueToken);

        if ($token !== null) {
            return $this->validateCsrfTokenInternal($token, $trueToken);
        } else {
            // 只要有一個為真,則返回真
            return ($this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken) || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken));
        }
}
返回true代表認證通過,false代表失敗,對於GET,HEAD', 'OPTIONS',這種方式是不認證的,返回true,預設通過,可以繼續訪問。

如果是其他的訪問方式,例如POST,那就的認證。
$trueToken = $this->loadCsrfToken();這個獲取完整COOKIE['_csrf']的真實存在的token。

看看$this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken);
裡面的這一句$this->getBodyParam($this->csrfParam)。就是獲取post過來的csrfToken的值或者表單的值,然後validateCsrfTokenInternal($token, $trueToken),這個函式將csrfToken進行解密(因為之前通過Yii::$app->request->csrfToken這個值的時候是加密的了,所以現在要解密)。解密之後的值如果和$trueToken相同的話就返回true。

再看看$this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken)這一句是通過$this->getCsrfTokenFromHeader()獲取head中的csrfToken的值,再進行解密,解密之後的值如果和$trueToken相同的話就返回true。

 注意的是return ($this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken) || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken));
 這一句是判斷($this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken) || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken))通過解密認證後就等效於return (true || false)的模式,也就是通過||判定括號裡的真假,只要有一個true,則返回true

所以說一旦生成token並儲存在COOKIE['_csrf']中,那麼每一次在訪問時,就會以這個token作為一個基準進行資料加密隨意生成一個csrfToken,然後返回給表單中。當post資料過來的時候,就得將這個csrfToken傳遞過來,然後進行解密,再和COOKIE['_csrf']的token進行比較,那麼如果相等就說明訪問是無攻擊性的,是本站的訪問。如果訪問不通過,說明可能刪改了一些資訊,是不安全的。