Yii中的csrf

什么是CSRF攻击

百度百科

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

防御措施

检查Referer字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

添加校验token

由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。

Yii2中csrf的实现原理

Yii2是使用token来预防csrf攻击的。

在配置文件中开启

'request' => [
            'enableCsrfValidation' => true,
        ],

校验token

在所有控制器继承的controller基类中,有个请求回调函数在请求处理前验证token。
在下面代码中,先判断是否开启了csrf验证,在判断目前异常处理器是否捕获了异常,最后才校验token

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;
    }

    return false;
}

接下来看看validateCsrfToken函数

public function validateCsrfToken($clientSuppliedToken = null)
{
    // 先判断http请求方法,如果是get、head、options方法则直接跳过,不认证。
    $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;
    }

    // 获取服务器原始的token
    $trueToken = $this->getCsrfToken();

    // 如果已经拿到了浏览器带过来的token,则直接与上面的服务器端原始的token进行比较
    if ($clientSuppliedToken !== null) {
        return $this->validateCsrfTokenInternal($clientSuppliedToken, $trueToken);
    }
    
    // 否则从请求body或者请求head中获取浏览器端的token,再进行比较
    return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken)
        || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
}

先看看服务器端原始的token是怎么来的.

从注释中可以看到,服务端生成的原始token一般是保存在session或者cookie中。然后浏览器会通过html表单的隐藏字段
或者http的header带给服务器,然后两者比较进行验证。

/**
 * 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)
{
    // 因为这个函数在一次请求中会多次调用,所以会保存token在_csrfToken字段中
    if ($this->_csrfToken === null || $regenerate) {
        $token = $this->loadCsrfToken();
        if ($regenerate || empty($token)) {
            // 重新生成token
            $token = $this->generateCsrfToken();
        }
        // 对token进行简单加密
        $this->_csrfToken = Yii::$app->security->maskToken($token);
    }

    return $this->_csrfToken;
}

看看loadCsrfToken函数,即从cookie或者session中获取服务端的token

/**
 * 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()
{
    // 默认是保存在cookie中的,比较不安全
    if ($this->enableCsrfCookie) {
        return $this->getCookies()->getValue($this->csrfParam);
    }

    return Yii::$app->getSession()->get($this->csrfParam);
}

那么token是怎么生成的呢?看下面代码可知token就是一个随机字符串[A-Za-z0-9_-],生成后保存在cookie中或者session中

/**
 * 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;
}

校验函数则比较简单了,就是解密后进行比较

private function validateCsrfTokenInternal($clientSuppliedToken, $trueToken)
{
    if (!is_string($clientSuppliedToken)) {
        return false;
    }

    $security = Yii::$app->security;

    return $security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken));
}

最后看看加解密函数

/**
 * Masks a token to make it uncompressible.
 * Applies a random mask to the token and prepends the mask used to the result making the string always unique.
 * Used to mitigate BREACH attack by randomizing how token is outputted on each request.
 * @param string $token An unmasked token.
 * @return string A masked token.
 * @since 2.0.12
 */
public function maskToken($token)
{
    // The number of bytes in a mask is always equal to the number of bytes in a token.
    $mask = $this->generateRandomKey(StringHelper::byteLength($token));
    return StringHelper::base64UrlEncode($mask . ($mask ^ $token));
}

/**
 * Unmasks a token previously masked by `maskToken`.
 * @param string $maskedToken A masked token.
 * @return string An unmasked token, or an empty string in case of token format is invalid.
 * @since 2.0.12
 */
public function unmaskToken($maskedToken)
{
    $decoded = StringHelper::base64UrlDecode($maskedToken);
    $length = StringHelper::byteLength($decoded) / 2;
    // Check if the masked token has an even length.
    if (!is_int($length)) {
        return '';
    }

    return StringHelper::byteSubstr($decoded, $length, $length) ^ StringHelper::byteSubstr($decoded, 0, $length);
}

值得一提的是,存在cookie中也是比较不安全的,但是要比存在session中高效??貌似这两种安全性差不多

最后,查看getCsrfToken函数的调用,发现有多次调用,比如登录的时候调用并且刷新token,生成表单的时候回调用。

总结

防止csrf攻击的主要核心在于如何确认该请求是否是自己方的代码产生的,而不是别人恶意代码伪造的。
token相当于一个身份的象征,一个请求对应一个token,从而确保请求的身份的认定。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,699评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,124评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,127评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,342评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,356评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,057评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,654评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,572评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,205评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,343评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,015评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,704评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,196评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,690评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,348评论 2 358

推荐阅读更多精彩内容