常見PHP框架CSRF防范方案分析

CSRF

什么是CSRF

CSRF(跨站請求偽造)是一種惡意的攻擊,它憑借已通過身份驗證的用戶身份來運(yùn)行未經(jīng)過授權(quán)的命令。網(wǎng)上有很多相關(guān)介紹了,具體攻擊方式就不細(xì)說了,下面來說說LaravelYii2是如何來做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,只是提供了配置不進(jìn)行CSRF驗證的路由的功能(這里是因為某些場景下是不需要進(jìn)行驗證的,例如微信支付的回調(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í)行的方法??梢钥吹街虚g件先判斷是否是讀請求,例如'HEAD', 'GET', 'OPTIONS',又或者是處于單元測試,又或者是不需要進(jìn)行驗證的路由,又或者token驗證通過,那就會把這個token設(shè)置到cookie里去(可以使用 cookie 值來設(shè)置 X-XSRF-TOKEN 請求頭,而一些 JavaScript 框架和庫(如 AngularAxios)會自動將這個值添加到 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ù)或者headerX-CSRF-TOKEN里去取token,取不到則取headerX-XSRF-TOKENX-CSRF-TOKENLaravel用到的,X-XSRF-TOKEN則是上面說的可能是框架自己去Cookie取,然后進(jìn)行設(shè)置的。這里之所以需要decrypt,是因為LaravelCookie是加密的。

取到參數(shù)里的token后就和session里的值進(jìn)行比較了,這里用到了hash_equals是為了防止時序攻擊。在 PHP 中比較字符串相等時如果使用雙等 == ,兩個字符串是從第一位開始逐一進(jìn)行比較的,發(fā)現(xiàn)不同就立即返回 false,那么通過計算返回的速度就知道了大概是哪一位開始不同的,這樣就可以按位破解。而使用 hash_equals 比較兩個字符串,無論字符串是否相等,函數(shù)的時間消耗是恒定的,這樣可以有效的防止時序攻擊。

上面就是Laravel進(jìn)行CSRF防范的方案了,大家讀到這里不知道有沒有發(fā)現(xiàn)一個問題,就是我們只看到了比較token,但是好像沒看到在哪里設(shè)置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的時候就會設(shè)置token了,并且一直用這同一個session,并不需要驗證完一次就換一次。

下面再來看下Yii2是如何處理的

Yii2 CSRF防范

Yii2基于版本2.0.18進(jìn)行分析。

生成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為空,或者需要重新生成,則去CookieSession取,取到為空或者需要更新,則重新生成,并存到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里取,并進(jìn)行比較。

其他方案

可以看到LaravelYii2都是要基于Session或者Cookie來進(jìn)行token存儲的,下面來介紹一直不需要存儲token的方案。

方案一 使用PHP強(qiáng)加密函數(shù)

生成token

//PASSWORD_DEFAULT - 使用 bcrypt 算法 (PHP 5.5.0 默認(rèn))。 注意,該常量會隨著 PHP 加入更新更高強(qiáng)度的算法而改變。 所以,使用此常量生成結(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來進(jìn)行處理,這里方案稍微有點(diǎn)不同,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.itdecent.cn/p/ded065584f63

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容