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

CSRF

什么是CSRF

CSRF(跨站請求偽造)是一種惡意的攻擊研乒,它憑借已通過身份驗證的用戶身份來運行未經(jīng)過授權(quán)的命令汹忠。網(wǎng)上有很多相關介紹了,具體攻擊方式就不細說了雹熬,下面來說說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隆判,只是提供了配置不進行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 框架和庫(如 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-TOKEN火鼻,X-CSRF-TOKENLaravel用到的,X-XSRF-TOKEN則是上面說的可能是框架自己去Cookie取,然后進行設置的魁索。這里之所以需要decrypt融撞,是因為LaravelCookie是加密的。

取到參數(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為空,或者需要重新生成灾锯,則去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里取金砍,并進行比較局蚀。

其他方案

可以看到LaravelYii2都是要基于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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蹦掐,隨后出現(xiàn)的幾起案子技羔,更是在濱河造成了極大的恐慌,老刑警劉巖卧抗,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堕阔,死亡現(xiàn)場離奇詭異,居然都是意外死亡颗味,警方通過查閱死者的電腦和手機超陆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浦马,“玉大人时呀,你說我怎么就攤上這事【” “怎么了谨娜?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長磺陡。 經(jīng)常有香客問我趴梢,道長漠畜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任坞靶,我火速辦了婚禮憔狞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘彰阴。我一直安慰自己瘾敢,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布尿这。 她就那樣靜靜地躺著簇抵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪射众。 梳的紋絲不亂的頭發(fā)上碟摆,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音叨橱,去河邊找鬼典蜕。 笑死,一個胖子當著我的面吹牛雏逾,可吹牛的內(nèi)容都是我干的嘉裤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼栖博,長吁一口氣:“原來是場噩夢啊……” “哼屑宠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仇让,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤典奉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后丧叽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卫玖,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年踊淳,在試婚紗的時候發(fā)現(xiàn)自己被綠了假瞬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡迂尝,死狀恐怖脱茉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垄开,我是刑警寧澤琴许,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站溉躲,受9級特大地震影響榜田,放射性物質(zhì)發(fā)生泄漏益兄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一箭券、第九天 我趴在偏房一處隱蔽的房頂上張望净捅。 院中可真熱鬧,春花似錦邦鲫、人聲如沸灸叼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至屁魏,卻和暖如春滔以,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氓拼。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工你画, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桃漾。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓坏匪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撬统。 傳聞我的和親對象是個殘疾皇子适滓,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353