Yii2 csrf综合


Yii2 csrf综合


正文

引言

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding, 通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。 其原理是攻击者构造网站后台某个功能接口的请求地址,诱导用户去点击或者用特殊方法让该请求地址自动加载。 用户在登录状态下这个请求被服务端接收后会被误以为是用户合法的操作。对于 GET 形式的接口地址可轻易被攻击, 对于 POST 形式的接口地址也不是百分百安全,攻击者可诱导用户进入带 Form 表单可用POST方式提交参数的页面。

XSS(Cross Site Scripting)跨站脚本攻击,恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时, 嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。

XSS和MySQL注入攻击类似,CSRF是诱导用户点击或提交网址实现攻击目的(可以看这里理解), 也可以看一下跨站请求伪造—CSRF

那么怎么防止CSRF攻击呢?

SameSite 属性

Cookie 的 SameSite 属性用来限制第三方 Cookie,从而减少安全风险,可以用来防止 CSRF 攻击和用户追踪。

它可以设置三个值。

Strict
Lax
None

同源检测

在 HTTP 协议中,每一个异步请求都会携带两个 Header ,用于标记来源域名:

Origin Header
Referer Header

验证码

CSRF 攻击往往是在用户不知情的情况下成功伪造请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求, 而且因为 CSRF 攻击无法获取到验证码,因此通常情况下,验证码能够很好地遏制 CSRF 攻击。

但验证码并不是万能的,因为出于用户体验考虑,不能给网站所有的操作都加上验证码。 因此,验证码只能作为防御 CSRF 的一种辅助手段,而不能作为最主要的解决方案。

添加 Token 验证

CSRF 攻击之所以能够成功,是因为攻击者可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 Cookie 中, 因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的 Cookie 来通过安全验证。

要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。 可以在 HTTP 请求中以参数的形式加入一个随机产生的 Token,并在服务器端建立一个拦截器来验证这个 Token, 如果请求中没有 Token 或者 Token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

有一种解决思路是采用了anti-csrf-token方案:

  1. 服务端在收到路由请求时,生成一个随机数,在渲染请求页面时把随机数埋入页面(一般埋入 form 表单内, <input type="hidden" name="_csrf_token" value="xxxx">
  2. 服务端设置setCookie,把该随机数作为cookie或者session种入用户浏览器
  3. 当用户发送 GET 或者 POST 请求时带上_csrf_token参数(对于 Form 表单直接提交即可, 因为会自动把当前表单内所有的 input 提交给后台,包括_csrf_token
  4. 后台在接受到请求后解析请求的cookie获取_csrf_token的值,然后和用户GET/POST请求提交的_csrf_token做个比较, 如果相等表示请求是合法的。

实际操作

我们项目中经常会用到ajax交互技术,但_csrf验证怎么办呢?我们需要在这次提交中同时提交csrf的信息,但是怎么获取呢?加入这个表单项:

<input type="hidden" name="_csrf" value="<?= Yii::$app->request->csrfToken ?>">

接下来,取值同时传过去就ok了。

csrf是为了防止跨站请求伪造攻击,在配置文件backend/config/main.php中可以设置request组件的csrfParam属性值:

'components' => [
    'request' => [
        'csrfParam' => '_csrf-backend',
    ],
],        

如果使用Yii2原生的表单工具:

<div class="col-lg-5">
    <?php $form = ActiveForm::begin(['id' => 'login-form']); ?>

        <?= $form->field($model, 'username')->textInput(['autofocus' => true]) ?>

        <?= $form->field($model, 'password')->passwordInput() ?>

        <?= $form->field($model, 'rememberMe')->checkbox() ?>

        <div class="form-group">
            <?= Html::submitButton('登录', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
        </div>

    <?php ActiveForm::end(); ?>
</div>

生成的html代码:

<div class="col-lg-5">
    <form id="login-form" action="/site/login.html" method="post" role="form">
        <input type="hidden" name="_csrf-backend" value="v5G4eIFl_Lel0yPkbWPqDw1l7jJWYknp9AXY6sF_xKeLwOIgyVXL1dyBELteFKtVTwqoAw8hFr-VSo2c9x2vyg==">
        <div class="form-group field-adminloginform-username required has-error">
            <label class="control-label" for="adminloginform-username">用户名</label>
            <input type="text" id="adminloginform-username" class="form-control" name="AdminLoginForm[username]" autofocus="" aria-required="true" aria-invalid="true">
            <p class="help-block help-block-error">用户名不能为空。</p>
        </div>
        <div class="form-group field-adminloginform-password required">
            <label class="control-label" for="adminloginform-password">密码</label>
            <input type="password" id="adminloginform-password" class="form-control" name="AdminLoginForm[password]" aria-required="true">
            <p class="help-block help-block-error"></p>
        </div>
        <div class="form-group field-adminloginform-rememberme">
            <div class="checkbox">
                <label for="adminloginform-rememberme">
                    <input type="hidden" name="AdminLoginForm[rememberMe]" value="0">
                    <input type="checkbox" id="adminloginform-rememberme" name="AdminLoginForm[rememberMe]" value="1" checked="">
                    记住密码
                </label>
                <p class="help-block help-block-error"></p>
            </div>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-primary" name="login-button">登录</button>                
        </div>
    </form>        
</div>

如果用ajax请求,csrf怎么赋值呢:

方法一:
<input type="hidden" name="_csrf" value="<?= Yii::$app->request->csrfToken ?>">

方法二:
// head头中有csrf-token
<meta name="csrf-token" content="v5G4eIFl_Lel0yPkbWPqDw1l7jJWYknp9AXY6sF_xKeLwOIgyVXL1dyBELteFKtVTwqoAw8hFr-VSo2c9x2vyg==">
var csrfToken = $('meta[name="csrf-token"]').attr("content");
$.ajax({
  type: 'POST',
  url: url,
  data: {_csrf:csrfToken},
  success: success,
  dataType: dataType
});

在特定控制器中不想用csrf,可以关闭:

方法一:
public $enableCsrfValidation = false;

方法二:
public function init() {
    $this->enableCsrfValidation = false;
}

生成追踪

看一下Yii2中csrf原理:

我们页面代码 /backend/views/layouts/main.php 中:

<?php
use backend\assets\AppAsset;
use yii\helpers\Html;

AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
    <meta charset="<?= Yii::$app->charset ?>">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <?= Html::csrfMetaTags() ?>
    <title>web</title>
    <?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>

<div class="wrap">
    <?= $content ?>
</div>

<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

就是这里生成的含有_csrf的head头:

<meta name="csrf-param" content="_csrf">
<meta name="csrf-token" content="ZHQ2LnlIQ3gvQg4WIz0XSigkR34yOgwrKTBvfxYudCkeEWleGgoVOg==">

yii\helpers\Html::csrfMetaTags() 调用父类 yii\helpers\BaseHtml(), yii\helpers\BaseHtml中有:

/**
 * 生成包含CSRF令牌信息的meta标签
 * Generates the meta tags containing CSRF token information.
 * @return string the generated meta tags
 * @see Request::enableCsrfValidation
 */
public static function csrfMetaTags()
{
    $request = Yii::$app->getRequest();
    if ($request instanceof Request && $request->enableCsrfValidation) {
        return static::tag('meta', '', ['name' => 'csrf-param', 'content' => $request->csrfParam]) . "\n    "
            . static::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getCsrfToken()]) . "\n";
    }

    return '';
}

调用request组件的getCsrfToken()方法,

yii\web\Request中有:

/**
 * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'.
 * This property is used only when [[enableCsrfValidation]] is true.
 */
public $csrfParam = '_csrf';

/**
 * 获取CSRF令牌
 * 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) {
        if ($regenerate || ($token = $this->loadCsrfToken()) === null) {
            // 第一次访问web,loadCsrfToken()获取不到值,返回null,生成一个随机的token值
            $token = $this->generateCsrfToken();
        }
        $this->_csrfToken = Yii::$app->security->maskToken($token);
    }

    return $this->_csrfToken;
}

/**
 * 从cookie 或 session 中获取CSRF令牌
 * 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);
}

/**
 * 生成用于执行CSRF验证的可见随机令牌,保存到session 或 cookie 中
 * 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);
        // 存入cookie
        Yii::$app->getResponse()->getCookies()->add($cookie);
    } else {
        // 存入session
        Yii::$app->getSession()->set($this->csrfParam, $token);
    }

    return $token;
}

/**
 * 使用随机生成的CSRF令牌创建cookie
 * Creates a cookie with a randomly generated CSRF token.
 * Initial values specified in [[csrfCookie]] will be applied to the generated cookie.
 * @param string $token the CSRF token
 * @return Cookie the generated cookie
 * @see enableCsrfValidation
 */
protected function createCsrfCookie($token)
{
    $options = $this->csrfCookie;
    $options['name'] = $this->csrfParam;
    $options['value'] = $token;
    return new Cookie($options);
}

security组件 yii\base\Security 中:

/**
 * 生成指定长度的随机字符串,生成的字符串与[A-Za-z0-9 _-] +匹配,并且对URL编码透明
 * 这个方法很常用,也很通用,在找回密码那里也用到这个方法生产随机字符串
 * Generates a random string of specified length.
 * The string generated matches [A-Za-z0-9_-]+ and is transparent to URL-encoding.
 *
 * @param int $length the length of the key in characters
 * @return string the generated random key
 * @throws Exception on failure.
 */
public function generateRandomString($length = 32)
{
    if (!is_int($length)) {
        throw new InvalidParamException('First parameter ($length) must be an integer');
    }

    if ($length < 1) {
        throw new InvalidParamException('First parameter ($length) must be greater than 0');
    }

    $bytes = $this->generateRandomKey($length);
    return substr(StringHelper::base64UrlEncode($bytes), 0, $length);
}

/**
 * 生成指定数量的随机字节,会有看不懂的字符出现
 * Generates specified number of random bytes.
 * Note that output may not be ASCII.
 * @see generateRandomString() if you need a string.
 *
 * @param int $length the number of bytes to generate
 * @return string the generated random bytes
 * @throws InvalidParamException if wrong length is specified
 * @throws Exception on failure.
 */
public function generateRandomKey($length = 32)
{
    if (!is_int($length)) {
        throw new InvalidParamException('First parameter ($length) must be an integer');
    }

    if ($length < 1) {
        throw new InvalidParamException('First parameter ($length) must be greater than 0');
    }

    // always use random_bytes() if it is available
    if (function_exists('random_bytes')) {
        return random_bytes($length);
    }

    // The recent LibreSSL RNGs are faster and likely better than /dev/urandom.
    // Parse OPENSSL_VERSION_TEXT because OPENSSL_VERSION_NUMBER is no use for LibreSSL.
    // https://bugs.php.net/bug.php?id=71143
    if ($this->_useLibreSSL === null) {
        $this->_useLibreSSL = defined('OPENSSL_VERSION_TEXT')
            && preg_match('{^LibreSSL (\d\d?)\.(\d\d?)\.(\d\d?)$}', OPENSSL_VERSION_TEXT, $matches)
            && (10000 * $matches[1]) + (100 * $matches[2]) + $matches[3] >= 20105;
    }

    // Since 5.4.0, openssl_random_pseudo_bytes() reads from CryptGenRandom on Windows instead
    // of using OpenSSL library. LibreSSL is OK everywhere but don't use OpenSSL on non-Windows.
    if ($this->_useLibreSSL
        || (
            DIRECTORY_SEPARATOR !== '/'
            && substr_compare(PHP_OS, 'win', 0, 3, true) === 0
            && function_exists('openssl_random_pseudo_bytes')
        )
    ) {
        $key = openssl_random_pseudo_bytes($length, $cryptoStrong);
        if ($cryptoStrong === false) {
            throw new Exception(
                'openssl_random_pseudo_bytes() set $crypto_strong false. Your PHP setup is insecure.'
            );
        }
        if ($key !== false && StringHelper::byteLength($key) === $length) {
            return $key;
        }
    }

    // mcrypt_create_iv() does not use libmcrypt. Since PHP 5.3.7 it directly reads
    // CryptGenRandom on Windows. Elsewhere it directly reads /dev/urandom.
    if (function_exists('mcrypt_create_iv')) {
        $key = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
        if (StringHelper::byteLength($key) === $length) {
            return $key;
        }
    }

    // If not on Windows, try to open a random device.
    if ($this->_randomFile === null && DIRECTORY_SEPARATOR === '/') {
        // urandom is a symlink to random on FreeBSD.
        $device = PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom';
        // Check random device for special character device protection mode. Use lstat()
        // instead of stat() in case an attacker arranges a symlink to a fake device.
        $lstat = @lstat($device);
        if ($lstat !== false && ($lstat['mode'] & 0170000) === 020000) {
            $this->_randomFile = fopen($device, 'rb') ?: null;

            if (is_resource($this->_randomFile)) {
                // Reduce PHP stream buffer from default 8192 bytes to optimize data
                // transfer from the random device for smaller values of $length.
                // This also helps to keep future randoms out of user memory space.
                $bufferSize = 8;

                if (function_exists('stream_set_read_buffer')) {
                    stream_set_read_buffer($this->_randomFile, $bufferSize);
                }
                // stream_set_read_buffer() isn't implemented on HHVM
                if (function_exists('stream_set_chunk_size')) {
                    stream_set_chunk_size($this->_randomFile, $bufferSize);
                }
            }
        }
    }

    if (is_resource($this->_randomFile)) {
        $buffer = '';
        $stillNeed = $length;
        while ($stillNeed > 0) {
            $someBytes = fread($this->_randomFile, $stillNeed);
            if ($someBytes === false) {
                break;
            }
            $buffer .= $someBytes;
            $stillNeed -= StringHelper::byteLength($someBytes);
            if ($stillNeed === 0) {
                // Leaving file pointer open in order to make next generation faster by reusing it.
                return $buffer;
            }
        }
        fclose($this->_randomFile);
        $this->_randomFile = null;
    }

    throw new Exception('Unable to generate a random key');
}

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

yii\helpers\StringHelper继承自yii\helpers\BaseStringHelper, BaseStringHelper有:

/**
 * 返回给定字符串的字节数,此方法可确保使用mb_strlen()将字符串视为字节数组
 * Returns the number of bytes in the given string.
 * This method ensures the string is treated as a byte array by using `mb_strlen()`.
 * @param string $string the string being measured for length
 * @return int the number of bytes in the given string.
 */
public static function byteLength($string)
{
    return mb_strlen($string, '8bit');
}

/**
 * 将字符串进行Base 64编码(使用对URL和文件名安全的字母)
 * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
 *
 * > Note: Base 64 padding `=` may be at the end of the returned string.
 * > `=` is not transparent to URL encoding.
 *
 * @see https://tools.ietf.org/html/rfc4648#page-7
 * @param string $input the string to encode.
 * @return string encoded string.
 * @since 2.0.12
 */
public static function base64UrlEncode($input)
{
    return strtr(base64_encode($input), '+/', '-_');
}

校验追踪

yii\web\Controller中有:

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

所有web应用控制器都继承自yii\web\Controller, 通过Yii::$app->getRequest()->validateCsrfToken()可以看出会调用 request 组件的validateCsrfToken()方法。

yii\web\Request中有:

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

    // 从cookie 、 session 中获取
    $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->unmaskToken($clientSuppliedToken) === $security->unmaskToken($trueToken);
}

/**
 * Returns the named request body parameter value.
 * If the parameter does not exist, the second parameter passed to this method will be returned.
 * @param string $name the parameter name
 * @param mixed $defaultValue the default parameter value if the parameter does not exist.
 * @return mixed the parameter value
 * @see getBodyParams()
 * @see setBodyParams()
 */
public function getBodyParam($name, $defaultValue = null)
{
    $params = $this->getBodyParams();

    return isset($params[$name]) ? $params[$name] : $defaultValue;
}

/**
 * @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent.
 */
public function getCsrfTokenFromHeader()
{
    return $this->headers->get(static::CSRF_HEADER);
}

/**
 * Returns the request parameters given in the request body.
 *
 * Request parameters are determined using the parsers configured in [[parsers]] property.
 * If no parsers are configured for the current [[contentType]] it uses the PHP function `mb_parse_str()`
 * to parse the [[rawBody|request body]].
 * @return array the request parameters given in the request body.
 * @throws \yii\base\InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]].
 * @see getMethod()
 * @see getBodyParam()
 * @see setBodyParams()
 */
public function getBodyParams()
{
    if ($this->_bodyParams === null) {
        if (isset($_POST[$this->methodParam])) {
            $this->_bodyParams = $_POST;
            unset($this->_bodyParams[$this->methodParam]);
            return $this->_bodyParams;
        }

        $rawContentType = $this->getContentType();
        if (($pos = strpos($rawContentType, ';')) !== false) {
            // e.g. text/html; charset=UTF-8
            $contentType = substr($rawContentType, 0, $pos);
        } else {
            $contentType = $rawContentType;
        }

        if (isset($this->parsers[$contentType])) {
            $parser = Yii::createObject($this->parsers[$contentType]);
            if (!($parser instanceof RequestParserInterface)) {
                throw new InvalidConfigException("The '$contentType' request parser is invalid. It must implement the yii\\web\\RequestParserInterface.");
            }
            $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType);
        } elseif (isset($this->parsers['*'])) {
            $parser = Yii::createObject($this->parsers['*']);
            if (!($parser instanceof RequestParserInterface)) {
                throw new InvalidConfigException('The fallback request parser is invalid. It must implement the yii\\web\\RequestParserInterface.');
            }
            $this->_bodyParams = $parser->parse($this->getRawBody(), $rawContentType);
        } elseif ($this->getMethod() === 'POST') {
            // PHP has already parsed the body so we have all params in $_POST
            $this->_bodyParams = $_POST;
        } else {
            $this->_bodyParams = [];
            mb_parse_str($this->getRawBody(), $this->_bodyParams);
        }
    }

    return $this->_bodyParams;
}

security组件 yii\base\Security 中:

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

yii\helpers\StringHelper继承自yii\helpers\BaseStringHelper, BaseStringHelper 有:

/**
 * Returns the number of bytes in the given string.
 * This method ensures the string is treated as a byte array by using `mb_strlen()`.
 * @param string $string the string being measured for length
 * @return int the number of bytes in the given string.
 */
public static function byteLength($string)
{
    return mb_strlen($string, '8bit');
}

/**
 * Returns the portion of string specified by the start and length parameters.
 * This method ensures the string is treated as a byte array by using `mb_substr()`.
 * @param string $string the input string. Must be one character or longer.
 * @param int $start the starting position
 * @param int $length the desired portion length. If not specified or `null`, there will be
 * no limit on length i.e. the output will be until the end of the string.
 * @return string the extracted part of string, or FALSE on failure or an empty string.
 * @see http://www.php.net/manual/en/function.substr.php
 */
public static function byteSubstr($string, $start, $length = null)
{
    return mb_substr($string, $start, $length === null ? mb_strlen($string, '8bit') : $length, '8bit');
}

/**
 * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
 *
 * @see https://tools.ietf.org/html/rfc4648#page-7
 * @param string $input encoded string.
 * @return string decoded string.
 * @since 2.0.12
 */
public static function base64UrlDecode($input)
{
    return base64_decode(strtr($input, '-_', '+/'));
}

简化一下解码过程就是:

$input = "v5G4eIFl_Lel0yPkbWPqDw1l7jJWYknp9AXY6sF_xKeLwOIgyVXL1dyBELteFKtVTwqoAw8hFr-VSo2c9x2vyg==";  // 加密后的_csrfToken

function byteSubstr($string, $start, $length = null) {
    return mb_substr($string, $start, $length === null ? mb_strlen($string, '8bit') : $length, '8bit');
}

$decoded = base64_decode(strtr($input, '-_', '+/'));
$length = mb_strlen($decoded, '8bit') / 2;

echo byteSubstr($decoded, $length, $length) ^ byteSubstr($decoded, 0, $length);  // 输出结果 4QZXH07byR3_3wAZBoF1YC_VaOUv6bkm

然后客户端 $clientSuppliedToken 与服务端 $trueToken 对比是否相等就知道是不是真实的请求了。






参考资料

跨站请求伪造—CSRF https://segmentfault.com/a/1190000021114673

Yii 2.0 权威指南 安全(Security): 安全领域的最佳实践(Best Practices) 防止 CSRF 攻击 https://www.yiichina.com/doc/guide/2.0/security-best-practices#csrf

https://www.cnblogs.com/bjfy/p/5939317.html

https://zhuanlan.zhihu.com/p/28870147

https://zhuanlan.zhihu.com/p/22521378

https://www.cnblogs.com/cnn2017/p/6800679.html

https://www.yiichina.com/tutorial/449

https://blog.csdn.net/u012979009/article/details/51790925

https://www.yiichina.com/code/927


返回