diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 2b299293f5..bd53f7fd1e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -210,6 +210,7 @@ Yii Framework 2 Change Log - Enh: Added support for array attributes in `in` validator (creocoder) - Enh: Improved `yii\helpers\Inflector::slug` to support more cases for Russian, Hebrew and special characters (samdark) - Enh: ListView now uses the widget ID in the base tag, consistent to gridview (cebe) +- Enh: Added `yii\web\Response::enableCsrfCookie` to support storing CSRF tokens in session (qiangxue) - Chg #2287: Split `yii\db\ColumnSchema::typecast()` into two methods `phpTypecast()` and `dbTypecast()` to allow specifying PDO type explicitly (cebe) - Chg #2898: `yii\console\controllers\AssetController` is now using hashes instead of timestamps (samdark) - Chg #2913: RBAC `DbManager` is now initialized via migration (samdark) diff --git a/framework/assets/yii.js b/framework/assets/yii.js index 350c8118ac..6b9aeccde1 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -71,6 +71,28 @@ yii = (function ($) { return $('meta[name=csrf-token]').prop('content'); }, + /** + * Sets the CSRF token in the meta elements. + * This method is provided so that you can update the CSRF token with the latest one you obtain from the server. + * @param name the CSRF token name + * @param value the CSRF token value + */ + setCsrfToken: function (name, value) { + $('meta[name=csrf-param]').prop('content', name); + $('meta[name=csrf-token]').prop('content', value) + }, + + /** + * Updates all form CSRF input fields with the latest CSRF token. + * This method is provided to avoid cached forms containing outdated CSRF tokens. + */ + refreshCsrfToken: function () { + var token = pub.getCsrfToken(); + if (token) { + $('form input[name="' + pub.getCsrfParam() + '"]').val(token); + } + }, + /** * Displays a confirmation dialog. * The default implementation simply displays a js confirmation dialog. @@ -211,6 +233,7 @@ yii = (function ($) { xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); } }); + pub.refreshCsrfToken(); } function initDataMethods() { diff --git a/framework/web/Request.php b/framework/web/Request.php index 71896ec025..f46a3c2d4d 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -115,10 +115,16 @@ class Request extends \yii\base\Request */ public $csrfParam = '_csrf'; /** - * @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true. - * @see Cookie + * @var array the configuration for creating the CSRF [[Cookie|cookie]]. This property is used only when + * both [[enableCsrfValidation]] and [[enableCsrfCookie]] are true. */ public $csrfCookie = ['httpOnly' => true]; + /** + * @var boolean whether to use cookie to persist CSRF token. If false, CSRF token will be stored + * in session under the name of [[csrfParam]]. Note that while storing CSRF tokens in session increases + * security, it requires starting a session for every page, which will degrade your site performance. + */ + public $enableCsrfCookie = true; /** * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true. */ @@ -1227,29 +1233,6 @@ class Request extends \yii\base\Request return $cookies; } - /** - * @var Cookie - */ - private $_csrfCookie; - - /** - * Returns the unmasked random token used to perform CSRF validation. - * This token is typically sent via a cookie. If such a cookie does not exist, a new token will be generated. - * @return string the random token for CSRF validation. - * @see enableCsrfValidation - */ - public function getRawCsrfToken() - { - if ($this->_csrfCookie === null) { - $this->_csrfCookie = $this->getCookies()->get($this->csrfParam); - if ($this->_csrfCookie === null || empty($this->_csrfCookie->value)) { - $this->_csrfCookie = $this->createCsrfCookie(); - Yii::$app->getResponse()->getCookies()->add($this->_csrfCookie); - } - } - return $this->_csrfCookie->value; - } - private $_csrfToken; /** @@ -1258,17 +1241,19 @@ class Request extends \yii\base\Request * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/). * This token may be passed along via a hidden field of an HTML form or an HTTP header value * to support CSRF validation. - * + * @param boolean $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() + public function getCsrfToken($regenerate = false) { - if ($this->_csrfToken === null) { + 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, self::CSRF_MASK_LENGTH); - - $token = $this->getRawCsrfToken(); // 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))); } @@ -1276,6 +1261,39 @@ class Request extends \yii\base\Request 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); + } else { + 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) { + $config = $this->csrfCookie; + $config['name'] = $this->csrfParam; + $config['value'] = $token; + Yii::$app->getResponse()->getCookies()->add(new Cookie($config)); + } else { + $token = Yii::$app->getSecurity()->generateRandomString(); + Yii::$app->getSession()->set($this->csrfParam, $token); + } + return $token; + } + /** * 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. @@ -1333,10 +1351,10 @@ class Request extends \yii\base\Request if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) { return true; } - $trueToken = $this->getCookies()->getValue($this->csrfParam); - $token = $this->getBodyParam($this->csrfParam); - return $this->validateCsrfTokenInternal($token, $trueToken) + $trueToken = $this->loadCsrfToken(); + + return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken) || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken); }