Commit 262a77ca by Qiang Xue

Added `yii\web\Response::enableCsrfCookie` to support storing CSRF tokens in session

parent 9c9ce298
......@@ -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)
......
......@@ -72,6 +72,28 @@ yii = (function ($) {
},
/**
* 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.
* You may override this by setting `yii.confirm`.
......@@ -211,6 +233,7 @@ yii = (function ($) {
xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken());
}
});
pub.refreshCsrfToken();
}
function initDataMethods() {
......
......@@ -115,11 +115,17 @@ 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.
*/
public $enableCookieValidation = 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)));
}
......@@ -1277,6 +1262,39 @@ class Request extends \yii\base\Request
}
/**
* 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.
* @param string $token1
......@@ -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);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment