yii2的csrf验证原理分析及token缓存解决方案

正文首要分七个部分,首先简单介绍csrf,接着对照源码重点解析一下yii框架的印证原理,最终针对页面缓存导致的token被缓存指出一种有效的方案。涉及的知识点会作为附录附于文末。

1. CSRF

  • CSRF: Cross-site request forgery, 跨站请求伪造,也被称为one-click
    attack或者session riding, 常常缩写为CSRF或者XSRF。
  • 是一种挟持用户在时下已登陆的web应用程序上推行非本意的操作的抨击方法。
  • 跟跨网页脚本(XSS)相比较,XSS利用的是用户对一定网站的相信,CSRF利用的是网站对用户网页浏览器的亲信。

Cross-site request forgery, also known as one-click attack or
session riding and abbreviated as CSRF or XSRF, is a type
of malicious exploit of a website where unauthorized commands are
transmitted from a user that the web application trusts. There are
many ways in which a malicious website can transmit such commands;
specially-crafted image tags, hidden forms, and JavaScript
XMLHttpRequests, for example, can all work without the user’s
interaction or even knowledge. Unlike cross-site scripting(XSS), which
exploits the trust a user has for a particular site, CSRF exploits the
trust that a site has in a user’s browser.

1.CSRF描述

2. 抨击的细节

  • 跨站请求攻击,简单地说,就是攻击者通过一些技术手段欺骗用户的浏览器去访问一个和谐曾经认证过的网站并推行一些操作(如发邮件,发音讯,甚至财产操作如转账和购进商品)。由于浏览器已经认证过,所以被访问的网站会以为是真正的用户操作而去履行。那里用了web中用户身份验证的一个破绽:大约的身份验证只可以保障请求发自某个用户的浏览器,却不可能确保请求我是用户自愿发出的。

CSRF全称为“Cross-Site Request
Forgery”,是在用户合法的SESSION内发起的口诛笔伐。黑客通过在网页中置放Web恶意请求代码,并引诱被害人访问该页面,当页面被访问后,请求在受害人不知情的景观下以被害人的官方地位发起,并执行黑客所企盼的动作。以下HTML代码提供了一个“删除产品”的效应:

2.1 攻击进度

  • 浏览者C登陆信任网站A。
  • 证实通过,在用户C处发生A的cookie并蕴藏在本地。
  • 用户在尚未登出A网站的动静下,访问危险网站B。
  • 一发千钧网站B需求用户C的浏览器访问第三方站点A,发出一个伸手。
  • 按照危险网站B的要求,用户C的浏览器带着表明通过前边世的Cookie访问网站A。
  • 网站A不精通这些请求是用户C的浏览器发出的,仍旧危险网站B发出的。因为浏览器会自动带上用户C的Cookie,所以网站A会依照用户的权限处理请求,那样网站B就直达了仿照用户操作的目的。
<a href="http://www.shop.com/delProducts.php?id=100" "javascript:return confirm('Are you sure?')">Delete</a>

3. 看守措施

假如程序员在后台从未对该“删除产品”请求做相应的合法性验证,只要用户访问了该链接,相应的产品即被删除,那么黑客可经过欺骗受害者访问带有以下恶意代码的网页,即可在受害人不知情的情景下删除相应的制品。

3.1 检查Referer字段

  • HTTP头中有一个Refer字段,这么些字段用以标明请求来源于哪个地点。在处理敏感数据请求时,平常来说,Referer字段应和呼吁的地方位于同一域名下。以转账为例,Referer字段地址平日应该是转载按钮所在的网页地址,应该也位于www.example.com下。如若是CSRF攻击传来的伸手,Referer字段会是带有恶意网址的地址,不会放在www.example.com以下,那时候服务器就能识别出恶意的拜会。

2.yii的csrf验证原理
/vendor/yiisoft/yii2/web/Request.php简写为Request.php

3.2 添加校验token

  • 由于CSRF的真相在于攻击者欺骗服务器,使服务器认为请求是经过认证的用户发送的。所以一旦要求在拜访敏感数据请求时,需求用户浏览器提供不保存在cookie中,并且攻击者不可以伪造的数目作为校验,那么攻击者就不可以再履行CSRF攻击。那种多少一般是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数(每一趟暴发的值都不比)。当客户端通过表单提交请求时,这几个伪乱数也一并交给上去以供验证。正常的访问时,客户端浏览器可以正确得到并传播那么些伪乱数,而经过CSRF传来的欺骗性攻击中,攻击者无从事先得知这一个伪乱数的值,服务器端就会因为校验token的值为空或者不当,拒绝这几个疑心请求。

/vendor/yiisoft/yii2/web/Controller.php简写为Controller.php

4. django中出现csrf验证失利解决办法

开启csrf验证

4.1 在django 项目标setting旅长csrf中间件屏蔽,不引进!

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

在控制器里将enableCsrfValidation为true,则控制器内存有操作都会开启验证,经常做法是将enableCsrfValidation为false,而将有些灵活操作设为true,开启局地验证。

4.2 在form表单中添加{% csrf_token%}

  • 在使用post方法的form中添加{% csrf_token %}

<div class="container">
    <div class="login">
    <label>用户登录</label>
    <form method="post" action="/user/login/">
        {% csrf_token %}
        账号:<input type="text" maxlength="20" name="username"><br>
        密码:<input type="password" maxlength="20" name="password"><br>
        {{ error }}
        <input type="submit" value="登录">
    </form>
    </div>
</div>
  • 在实际上运行进程中,csrf_token被替换成了如下代码

<div class="container">
    <div class="login">
    <label>用户登录</label>
    <form method="post" action="/user/login/">
        <input type='hidden' name='csrfmiddlewaretoken' value='HedcdWXjTwCxyVOd4d8MKLmiCTHZpmU0X0nkQ6dzbBcMawOAwkNdDjZtariC2i1B' />
        账号:<input type="text" maxlength="20" name="username"><br>
        密码:<input type="password" maxlength="20" name="password"><br>

        <input type="submit" value="登录">
    </form>
    </div>
</div>
  • 自己利用的django版本是1.11.6。通过选拔Fiddler软件抓包分析,在通信进度中窥见请求头中cookie字段有了csrftoken值。

Request sent 137 bytes of Cookie data:

__lnkrntdmcvrd=-1
csrftoken=Htr860vr38EaEumkfWuvEoW3BmyXInDcXfBgJaLHldepg5mHH39WxWze9U9AljKN
sessionid=qjt57gldisr5gb6x5k663l9tv1axl86f

于是得以得出结论django中的csrf验证须要form中的隐藏字段和cookie中的csrftoken一同发给服务器达成csrf验证。

public $enableCsrfValidation = false;
/**
 * @param \yii\base\Action $action
 * @return bool
 * @desc: 局部开启csrf验证(重要的表单提交必须加入验证,加入$accessActions即可
 */
public function beforeAction($action){
    $currentAction = $action->id;
    $accessActions = ['vote','like','delete','download'];
    if(in_array($currentAction,$accessActions)) {
        $action->controller->enableCsrfValidation = true;
    }
    parent::beforeAction($action);
    return true;
}

P.S.

  • 前者中从不断然的平安,前端中浏览器和服务器通讯进度中的数据都是足以伪造的。

生成token字段

在Request.php

首先通过安全组件Security获取一个32位的擅自字符串,并存入cookie或session,那是原生的token.

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

随即通过一密密麻麻加密替换操作,生成加密后_csrfToken,这些是传给浏览器的token.
先随机爆发CSRF_MASK_LENGTH(Yii2里默许是8位)长度的字符串 mask

对mask和token举行如下运算 str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask))); $this->xorTokens($arg1,$arg2) 是一个先补位异或运算

/**
 * Returns the XOR result of two strings.
 * If the two strings are of different lengths, the shorter one will be padded to the length of the longer one.
 * @param string $token1
 * @param string $token2
 * @return string the XOR result
 */
private function xorTokens($token1, $token2)
{
    $n1 = StringHelper::byteLength($token1);
    $n2 = StringHelper::byteLength($token2);
    if ($n1 > $n2) {
        $token2 = str_pad($token2, $n1, $token2);
    } elseif ($n1 < $n2) {
        $token1 = str_pad($token1, $n2, $n1 === 0 ? ' ' : $token1);
    }
    return $token1 ^ $token2;
}
public function getCsrfToken($regenerate = false)
{
    if ($this->_csrfToken === null || $regenerate) {
        if ($regenerate || ($token = $this->loadCsrfToken()) === null) {
            $token = $this->generateCsrfToken();
        }
        // the mask doesn't need to be very random
        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.';
        $mask = substr(str_shuffle(str_repeat($chars, 5)), 0, static::CSRF_MASK_LENGTH);
        // The + sign may be decoded as blank space later, which will fail the validation
        $this->_csrfToken = str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask)));
    }

    return $this->_csrfToken;
}

验证token

在controller.php里调用request.php里的validateCsrfToken方法

/**
 * @inheritdoc
 */
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;
}
public function validateCsrfToken($token = null)
{
    $method = $this->getMethod();
    if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
        return true;
    }

    $trueToken = $this->loadCsrfToken();//如果开启了enableCsrfCookie,CsrfToken就从cookie里取,否者从session里取(更安全)

    if ($token !== null) {
        return $this->validateCsrfTokenInternal($token, $trueToken);
    } else {
        return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken)
            || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
    }
}

收获客户端传入

$this->getBodyParam($this->csrfParam)

然后是validateCsrfTokenInternal

private function validateCsrfTokenInternal($token, $trueToken)
{
    if (!is_string($token)) {
        return false;
    }
    $token = base64_decode(str_replace('.', '+', $token));
    $n = StringHelper::byteLength($token);
    if ($n <= static::CSRF_MASK_LENGTH) {
        return false;
    }
    $mask = StringHelper::byteSubstr($token, 0, static::CSRF_MASK_LENGTH);
    $token = StringHelper::byteSubstr($token, static::CSRF_MASK_LENGTH, $n - static::CSRF_MASK_LENGTH);
    $token = $this->xorTokens($mask, $token);

    return $token === $trueToken;
}

加密时用的是 str_replace('+', '.', base64_encode(mask.mask.this->xorTokens(token,token,mask))); 解密
1.第一要把.替换成+ 2.然后base64_decode 再
根据长度分别取出mask和mask和this->xorTokens(token,token,mask) ;
为了验证方便 this−>xorTokens(this−>xorTokens(token, $mask)
这里名为 token1 然后 举行mask和token1的异或运算,即得token
注意在加密时

token1=token^mask

所以 解密时

token=mask^token1=mask^(token^mask)

3.token缓存的化解方案

当页面全体被缓存后,token也被缓存导致验证败北,一种普遍的解决思路是每一遍提交前重复赢得token,那样就足以通过验证了。

附录:

str_pad(),该函数再次回到 input
被从左端、右端或者同时两端被填充到制定长度后的结果。假如可选的
pad_string 参数没有被指定,input 将被空格字符填充,否则它将被
pad_string 填充到指定长度;

str_shuffle() 函数打乱一个字符串,使用别的一种可能的排序方案。

因为yii2 csrf的表达的加解密
涉及到异或运算

故而必要先补充php里字符串异或运算的相干文化,不要求的可以跳过

^异或运算 不等同重临1 否者再次回到 0
在PHP语言中,日常用来做加密的演算,解密也平昔用^就行 字符串运算时
利用字符的ascii码转换为2进制来运算 单个字符运算

1.对此单个字符和单个字符的
直接总结其结果即可 比如表里的a^b

2.对此长度一样的三个字符串 如表里的ab^cd
计算a^c对应的结果和和b^d对应的结果 对应的字符连接起来

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图