什么是CSRF
CSRF
(跨站請求偽造)是一種惡意的攻擊研乒,它憑借已通過身份驗證的用戶身份來運行未經(jīng)過授權(quán)的命令汹忠。網(wǎng)上有很多相關介紹了,具體攻擊方式就不細說了雹熬,下面來說說Laravel
和Yii2
是如何來做CSRF
攻擊防范的宽菜。
Laravel CSRF防范
本次對Laravel
CSRF
防范源碼的分析是基于5.4.36
版本的,其他版本代碼可能有所不同竿报,但原理是相似的赋焕。
Laravel
通過中間件app/Http/Middleware/VerifyCsrfToken.php
來做CSRF
防范,來看下源碼仰楚。
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}
可以發(fā)現(xiàn)它是直接繼承了Illuminate\Foundation\Http\Middleware\VerifyCsrfToken
隆判,只是提供了配置不進行CSRF
驗證的路由的功能(這里是因為某些場景下是不需要進行驗證的,例如微信支付的回調(diào))僧界。
再來看下Illuminate\Foundation\Http\Middleware\VerifyCsrfToken
主要源碼侨嘀。
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Illuminate\Session\TokenMismatchException
*/
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return $this->addCookieToResponse($request, $next($request));
}
throw new TokenMismatchException;
}
這里handle
是所有路由經(jīng)過都會執(zhí)行的方法∥娼螅可以看到中間件先判斷是否是讀請求咬腕,例如'HEAD', 'GET', 'OPTIONS'
,又或者是處于單元測試葬荷,又或者是不需要進行驗證的路由涨共,又或者token
驗證通過,那就會把這個token
設置到cookie
里去(可以使用 cookie
值來設置 X-XSRF-TOKEN
請求頭宠漩,而一些 JavaScript
框架和庫(如 Angular
和 Axios
)會自動將這個值添加到 X-XSRF-TOKEN
頭中)举反。
我們再來看下是怎么驗證token
的。
/**
* Determine if the session and input CSRF tokens match.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function tokensMatch($request)
{
$token = $this->getTokenFromRequest($request);
return is_string($request->session()->token()) &&
is_string($token) &&
hash_equals($request->session()->token(), $token);
}
/**
* Get the CSRF token from the request.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function getTokenFromRequest($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
$token = $this->encrypter->decrypt($header);
}
return $token;
}
這里會從輸入?yún)?shù)或者header
的X-CSRF-TOKEN
里去取token
扒吁,取不到則取header
的X-XSRF-TOKEN
火鼻,X-CSRF-TOKEN
是Laravel
用到的,X-XSRF-TOKEN
則是上面說的可能是框架自己去Cookie
取,然后進行設置的魁索。這里之所以需要decrypt
融撞,是因為Laravel
的Cookie
是加密的。
取到參數(shù)里的token
后就和session
里的值進行比較了粗蔚,這里用到了hash_equals
是為了防止時序攻擊尝偎。在 PHP
中比較字符串相等時如果使用雙等 ==
,兩個字符串是從第一位開始逐一進行比較的鹏控,發(fā)現(xiàn)不同就立即返回 false
冬念,那么通過計算返回的速度就知道了大概是哪一位開始不同的,這樣就可以按位破解牧挣。而使用 hash_equals
比較兩個字符串急前,無論字符串是否相等,函數(shù)的時間消耗是恒定的瀑构,這樣可以有效的防止時序攻擊裆针。
上面就是Laravel
進行CSRF
防范的方案了,大家讀到這里不知道有沒有發(fā)現(xiàn)一個問題寺晌,就是我們只看到了比較token
世吨,但是好像沒看到在哪里設置token
。
從上面可以知道token
是存在session
里的呻征,那我們?nèi)タ聪?code>vendor/laravel/framework/src/Illuminate/Session/Store.php源碼耘婚。
/**
* Start the session, reading the data from a handler.
*
* @return bool
*/
public function start()
{
$this->loadSession();
if (! $this->has('_token')) {
$this->regenerateToken();
}
return $this->started = true;
}
可以看到Laravel
在啟動session
的時候就會設置token
了,并且一直用這同一個session
陆赋,并不需要驗證完一次就換一次沐祷。
下面再來看下Yii2
是如何處理的
Yii2 CSRF防范
Yii2
基于版本2.0.18
進行分析。
生成token
通過Yii::$app->request->getCsrfToken()
生成攒岛,我們?nèi)タ聪?code>vendor/yiisoft/yii2/web/Request.php源碼赖临。
/**
* 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)
{
if ($this->_csrfToken === null || $regenerate) {
$token = $this->loadCsrfToken();
if ($regenerate || empty($token)) {
$token = $this->generateCsrfToken();
}
$this->_csrfToken = Yii::$app->security->maskToken($token);
}
return $this->_csrfToken;
}
/**
* 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()
{
if ($this->enableCsrfCookie) {
return $this->getCookies()->getValue($this->csrfParam);
}
return Yii::$app->getSession()->get($this->csrfParam);
}
/**
* 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;
}
這里判斷請求里的token
為空,或者需要重新生成灾锯,則去Cookie
和Session
取兢榨,取到為空或者需要更新,則重新生成顺饮,并存到Cookie
或者Session
里去吵聪,并加密后返回。這樣第一次取的時候就會新生成一個token
兼雄,后面的請求則都是通過Cookie
或者Session
取吟逝。
下面來看下是怎么驗證的
/**
* Performs the CSRF validation.
*
* This method will validate the user-provided CSRF token by comparing it with the one stored in cookie or session.
* This method is mainly called in [[Controller::beforeAction()]].
*
* Note that the method will NOT perform CSRF validation if [[enableCsrfValidation]] is false or the HTTP method
* is among GET, HEAD or OPTIONS.
*
* @param string $clientSuppliedToken the user-provided CSRF token to be validated. If null, the token will be retrieved from
* the [[csrfParam]] POST field or HTTP header.
* This parameter is available since version 2.0.4.
* @return bool whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true.
*/
public function validateCsrfToken($clientSuppliedToken = null)
{
$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;
}
$trueToken = $this->getCsrfToken();
if ($clientSuppliedToken !== null) {
return $this->validateCsrfTokenInternal($clientSuppliedToken, $trueToken);
}
return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken)
|| $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
}
/**
* Validates CSRF token.
*
* @param string $clientSuppliedToken The masked client-supplied token.
* @param string $trueToken The masked true token.
* @return bool
*/
private function validateCsrfTokenInternal($clientSuppliedToken, $trueToken)
{
if (!is_string($clientSuppliedToken)) {
return false;
}
$security = Yii::$app->security;
return $security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken));
}
也是先判斷是否在需要驗證的請求里,是的話君旦,如果有提供token
澎办,直接拿來驗證嘲碱,否則就去請求參數(shù)或者header
里取金砍,并進行比較局蚀。
其他方案
可以看到Laravel
和Yii2
都是要基于Session
或者Cookie
來進行token
存儲的,下面來介紹一直不需要存儲token
的方案恕稠。
方案一 使用PHP強加密函數(shù)
生成token
//PASSWORD_DEFAULT - 使用 bcrypt 算法 (PHP 5.5.0 默認)琅绅。 注意,該常量會隨著 PHP 加入更新更高強度的算法而改變鹅巍。 所以千扶,使用此常量生成結(jié)果的長度將在未來有變化。 因此骆捧,數(shù)據(jù)庫里儲存結(jié)果的列可超過60個字符(最好是255個字符)澎羞。
const PASSWORD='csrf_password_0t1XkA8pw9dMXTpOq';
$uid=1;
$hash = password_hash(PASSWORD.$uid, PASSWORD_DEFAULT);
//生成hash類似 $2y$10$cWojK6D9530PXvx.tG4BuOX4.i1WVZf2D7d.bE3B5x4F1/j2e0XeG
//生成token傳給前端做csrf防范的token
$token =urlencode(base64_encode($hash));
驗證token
//驗token敛苇,true為通過
return password_verify(PASSWORD.$uid, base64_decode(urldecode($token)));
方案二 使用Md5函數(shù)
上面的方案妆绞,保密性好,但是加解密很耗性能枫攀,實際上可以用md5
來進行處理括饶,這里方案稍微有點不同,md5
需要手動加鹽来涨。
生成token
//把鹽連接到哈希值后面也一起給到前端
$salt = str_random('12');
$token = md5(self::MD5_PASSWORD . $salt . $uid) . '.' . $salt;
$token = urlencode(base64_encode($token));
驗證token
$token = base64_decode(urldecode($token));
$token_array = explode('.', $token);
return count($token_array) && $token_array[0]==md5(self::MD5_PASSWORD . $token_array[1] . $uid));
Enjoy it !
如果覺得文章對你有用图焰,可以贊助我喝杯咖啡~
版權(quán)聲明
轉(zhuǎn)載請注明作者和文章出處
作者: X先生
首發(fā)于http://www.reibang.com/p/ded065584f63