当前位置: 首页 > 后端技术 > PHP

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

时间:2023-03-30 01:12:55 PHP

什么是CSRFCSRF(cross-siterequestforgery)是一种依赖经过身份验证的用户身份运行未授权命令的恶意攻击。网上有很多相关介绍,具体的攻击方式我就不赘述了。下面说说Laravel和Yii2是如何防止CSRF攻击的。LaravelCSRFprevention本篇LaravelCSRFprevention源码分析基于5.4.36版本。其他版本的代码可能不一样,但是原理是差不多的。Laravel使用中间件app/Http/Middleware/VerifyCsrfToken.php来防止CSRF,我们来看源码。isReading($request)||$this->runningUnitTests()||$this->inExceptArray($request)||$this->tokensMatch($request)){返回$this->addCookieToResponse($request,$下一个($请求));抛出新的TokenMismatchException;}这里的handle是所有路由通过后执行的方法。可以看到中间件首先判断是不是读请求,比如'HEAD','GET','OPTIONS',或者是在单元测试中,或者是不需要验证的路由,或者token验证通过,则cookie中会设置这个token(可以使用cookie值设置X-XSRF-TOKEN请求头,一些JavaScript框架和库(如Angular和Axios)会自动加上这个X-XSRF-TOKEN标头的值)。让我们看看如何验证令牌。/***确定会话和输入CSRF令牌是否匹配。**@param\Illuminate\Http\Request$request*@returnbool*/保护函数tokensMatch($request){$token=$this->getTokenFromRequest($request);返回is_string($request->session()->token())&&is_string($token)&&hash_equals($request->session()->token(),$token);}/***从请求中获取CSRF令牌。**@param\Illuminate\Http\Request$request*@returnstring*/保护函数getTokenFromRequest($request){$token=$request->input('_token')?:$request->header('X-CSRF-令牌');如果(!$token&&$header=$request->header('X-XSRF-TOKEN')){$token=$this->encrypter->decrypt($header);}返回$令牌;}这里会从输入参数或header的X-CSRF-TOKEN里去取token,取不到则取header的X-XSRF-TOKEN,X-CSRF-TOKEN是Laravel使用的,X-XSRF-TOKEN是上面提到的框架使用的,可能框架自己获取,然后设置。这里之所以需要decrypt是因为Laravel的cookie是加密的。获取参数中的token后,与session中的值进行比较。这里使用hash_equals来防止定时攻击。如果在PHP中比较字符串时使用doubleequal==,两个字符串从第一位开始一个一个比较,如果不同,则立即返回false,那么通过计算返回就可以知道大致是哪个了speed位开始不同,这样就可以一点一点破解。而使用hash_equals比较两个字符串,无论字符串是否相等,函数耗时都是恒定的,可以有效防止时序攻击。以上就是Laravel的CSRF预防方案。看完这里,不知道大家有没有发现一个问题,就是我们只看到了比较token,但是好像没有看到在哪里设置token。从上面我们可以知道token是存在于session中的,那么我们来看下vendor/laravel/framework/src/Illuminate/Session/Store.php源码。/***启动会话,从处理程序读取数据。**@returnbool*/publicfunctionstart(){$this->loadSession();如果(!$this->has('_token')){$this->regenerateToken();}返回$this->started=true;可以看到Laravel会在启动session的时候设置token,一直使用同一个session,不需要认证一次再改。下面来看看Yii2是如何处理的Yii2CSRF防范Yii2是基于2.0.18版本进行分析的。生成的token是通过Yii::$app->request->getCsrfToken()生成的,我们看vendor/yiisoft/yii2/web/Request.php的源码。/***返回用于执行CSRF验证的令牌。**此令牌以防止[BREACH攻击](http://breachattack.com/)的方式生成。它可以通过HTML表单的隐藏字段或HTTP标头值传递*以支持CSRF验证。*@parambool$regenerate是否重新生成CSRFtoken。当这个参数为真时,每次*这个方法被调用时,都会生成一个新的CSRFtoken并持久化(在session或cookie中)。*@returnstring用于执行CSRF验证的令牌。*/publicfunctiongetCsrfToken($regenerate=false){if($this->_csrfToken===null||$regenerate){$token=$this->loadCsrfToken();如果($regenerate||空($token)){$token=$this->generateCsrfToken();$this->_csrfToken=Yii::$app->security->maskToken($token);}返回$this->_csrfToken;}/***从cookie或会话中加载CSRF令牌。*@returnstring从cookie或会话加载的CSRF令牌。如果cookie或会话*没有CSRF令牌,则返回Null。*/protectedfunctionloadCsrfToken(){if($this->enableCsrfCookie){return$this->getCookies()->getValue($this->csrfParam);}}返回Yii::$app->getSession()->get($this->csrfParam);}/***生成一个未屏蔽的随机令牌,用于执行CSRF验证。*@returnstring用于CSRF验证的随机标记。*/protectedfunctiongenerateCsrfToken(){$token=Yii::$app->getSecurity()->generateRandomString();如果($this->enableCsrfCookie){$cookie=$this->createCsrfCookie($token);Yii::$app->getResponse()->getCookies()->add($cookie);}else{Yii::$app->getSession()->set($this->csrfParam,$token);}返回$令牌;}这里判断请求中的token为空,或者需要重新生成,然后去Cookie和Session中获取,如果为空或者需要更新,则重新生成并保存到Cookie或Session,加密后返回,这样第一次获取时会生成新的token,后续请求通过Cookie或Session获取。下面来看看下面是怎么验证的/***执行CSRF验证。**此方法将通过将用户提供的CSRF令牌与存储在cookie或会话中的令牌进行比较来验证它。*该方法主要在[[Controller::beforeAction()]]中调用。**请注意,如果[[enableCsrfValidation]]为假或HTTP方法*在GET、HEAD或OPTIONS之间,该方法将不会执行CSRF验证。**@paramstring$clientSuppliedToken要验证的用户提供的CSRF令牌。如果为空,将从*[[csrfParam]]POST字段或HTTP标头中检索令牌。*此参数自版本2.0.4起可用。*@returnboolCSRFtoken是否有效。如果[[enableCsrfValidation]]为false,此方法将返回true。*/publicfunctionvalidateCsrfToken($clientSuppliedToken=null){$method=$this->getMethod();//仅在非“安全”方法上验证CSRF令牌https://tools.ietf。org/html/rfc2616#section-9.1.1if(!$this->enableCsrfValidation||in_array($method,['GET','HEAD','OPTIONS'],true)){返回真;$trueToken=$this->getCsrfToken();如果($clientSuppliedToken!==null){返回$this->validateCsrfTokenInternal($clientSuppliedToken,$trueToken);}返回$this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam),$trueToken)||$this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(),$trueToken);}/***验证CSRF令牌。**@paramstring$clientSuppliedToken屏蔽的客户端提供的令牌。*@paramstring$trueToken被屏蔽的真实令牌。*@returnbool*/privatefunctionvalidateCsrfTokenInternal($clientSuppliedToken,$trueToken){if(!is_string($clientSuppliedToken)){返回假;}$security=Yii::$app->s安全;返回$security->compareString($security->unmaskToken($clientSuppliedToken),$security->unmaskToken($trueToken));}也是先判断是否在request中需要验证。如果是,如果提供了token,直接使用它进行验证,否则,去请求参数或者header,对比其他方案。可以看到Laravel和Yii2都需要根据Session或者Cookie来存储token。以下是不需要存储令牌的解决方案。方案1使用PHP强加密函数生成token//PASSWORD_DEFAULT——使用bcrypt算法(PHP5.5.0默认)。请注意,随着PHP添加更新、更强大的算法,此常量将发生变化。因此,以后使用这个常量生成的结果长度会发生变化。因此,数据库中存储结果的列可以超过60个字符(最好是255个字符)。constPASSWORD='csrf_password_0t1XkA8pw9dMXTpOq';$uid=1;$hash=password_hash(PASSWORD.$uid,PASSWORD_DEFAULT);//生成类似于$2y$10$cWojK6D9530PXvx.tG4BuOX4.i1WVZf2D7d.btoGenrfkento$gento预防ckento/B5令牌的散列=urlencode(base64_encode($hash));verifytoken//验证令牌,true表示通过returnpassword_verify(PASSWORD.$uid,base64_decode(urldecode($token)));方案2使用Md5函数上述方案保密性较好,但加密和解密对性能消耗较大。其实可以用md5来处理。这里的scheme略有不同,md5需要手动加salt。生成token//将salt连接到hash值,一起交给前端$salt=str_random('12');$token=md5(self::MD5_PASSWORD.$salt.$uid)。'.'.$salt;$token=urlencode(base64_encode($token));验证令牌$token=base64_decode(urldecode($token));$token_array=explode('.',$token);returncount($token_array)&&$token_array[0]==md5(self::MD5_PASSWORD.$token_array[1].$uid));尽情享受吧!如果觉得文章对你有用,可以赞助我喝杯咖啡~版权声明转载请注明作者及文章出处:X先生https://segmentfault.com/a/1190000022707593