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 ...@@ -210,6 +210,7 @@ Yii Framework 2 Change Log
- Enh: Added support for array attributes in `in` validator (creocoder) - 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: 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: 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 #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 #2898: `yii\console\controllers\AssetController` is now using hashes instead of timestamps (samdark)
- Chg #2913: RBAC `DbManager` is now initialized via migration (samdark) - Chg #2913: RBAC `DbManager` is now initialized via migration (samdark)
......
...@@ -72,6 +72,28 @@ yii = (function ($) { ...@@ -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. * Displays a confirmation dialog.
* The default implementation simply displays a js confirmation dialog. * The default implementation simply displays a js confirmation dialog.
* You may override this by setting `yii.confirm`. * You may override this by setting `yii.confirm`.
...@@ -211,6 +233,7 @@ yii = (function ($) { ...@@ -211,6 +233,7 @@ yii = (function ($) {
xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken());
} }
}); });
pub.refreshCsrfToken();
} }
function initDataMethods() { function initDataMethods() {
......
...@@ -115,11 +115,17 @@ class Request extends \yii\base\Request ...@@ -115,11 +115,17 @@ class Request extends \yii\base\Request
*/ */
public $csrfParam = '_csrf'; public $csrfParam = '_csrf';
/** /**
* @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true. * @var array the configuration for creating the CSRF [[Cookie|cookie]]. This property is used only when
* @see Cookie * both [[enableCsrfValidation]] and [[enableCsrfCookie]] are true.
*/ */
public $csrfCookie = ['httpOnly' => 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. * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true.
*/ */
public $enableCookieValidation = true; public $enableCookieValidation = true;
...@@ -1227,29 +1233,6 @@ class Request extends \yii\base\Request ...@@ -1227,29 +1233,6 @@ class Request extends \yii\base\Request
return $cookies; 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; private $_csrfToken;
/** /**
...@@ -1258,17 +1241,19 @@ class Request extends \yii\base\Request ...@@ -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 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 * This token may be passed along via a hidden field of an HTML form or an HTTP header value
* to support CSRF validation. * 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. * @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 // the mask doesn't need to be very random
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.';
$mask = substr(str_shuffle(str_repeat($chars, 5)), 0, self::CSRF_MASK_LENGTH); $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 // 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))); $this->_csrfToken = str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask)));
} }
...@@ -1277,6 +1262,39 @@ class Request extends \yii\base\Request ...@@ -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. * 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. * 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 $token1
...@@ -1333,10 +1351,10 @@ class Request extends \yii\base\Request ...@@ -1333,10 +1351,10 @@ class Request extends \yii\base\Request
if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) { if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
return 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); || $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