什麼是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驗證通過。