diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 152b3e9..6bebff1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -31,6 +31,7 @@ Yii Framework 2 Change Log - Enh #1581: Added `ActiveQuery::joinWith()` and `ActiveQuery::innerJoinWith()` to support joining with relations (qiangxue) - Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Enh #1611: Added `BaseActiveRecord::markAttributeDirty()` (qiangxue) +- Enh #1634: Use masked CSRF tokens to prevent BREACH exploits (qiangxue) - Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) diff --git a/framework/yii/helpers/BaseHtml.php b/framework/yii/helpers/BaseHtml.php index 49fe832..b3a88c1 100644 --- a/framework/yii/helpers/BaseHtml.php +++ b/framework/yii/helpers/BaseHtml.php @@ -241,7 +241,7 @@ class BaseHtml $method = 'post'; } if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { - $hiddenInputs[] = static::hiddenInput($request->csrfVar, $request->getCsrfToken()); + $hiddenInputs[] = static::hiddenInput($request->csrfVar, $request->getMaskedCsrfToken()); } } diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php index 8849ed3..aae2e3c 100644 --- a/framework/yii/web/Request.php +++ b/framework/yii/web/Request.php @@ -10,6 +10,7 @@ namespace yii\web; use Yii; use yii\base\InvalidConfigException; use yii\helpers\Security; +use yii\helpers\StringHelper; /** * The web Request class represents an HTTP request @@ -83,6 +84,10 @@ class Request extends \yii\base\Request * The name of the HTTP header for sending CSRF token. */ const CSRF_HEADER = 'X-CSRF-Token'; + /** + * The length of the CSRF token mask. + */ + const CSRF_MASK_LENGTH = 8; /** @@ -1021,6 +1026,43 @@ class Request extends \yii\base\Request return $this->_csrfCookie->value; } + private $_maskedCsrfToken; + + /** + * Returns the masked CSRF token. + * This method will apply a mask to [[csrfToken]] so that the resulting CSRF token + * will not be exploited by [BREACH attacks](http://breachattack.com/). + * @return string the masked CSRF token. + */ + public function getMaskedCsrfToken() + { + if ($this->_maskedCsrfToken === null) { + $token = $this->getCsrfToken(); + $mask = Security::generateRandomKey(self::CSRF_MASK_LENGTH); + $this->_maskedCsrfToken = base64_encode($mask . $this->xorTokens($token, $mask)); + } + return $this->_maskedCsrfToken; + } + + /** + * 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, $token1); + } + return $token1 ^ $token2; + } + /** * @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. */ @@ -1072,6 +1114,20 @@ class Request extends \yii\base\Request $token = $this->getPost($this->csrfVar); break; } - return $token === $trueToken || $this->getCsrfTokenFromHeader() === $trueToken; + return $this->validateCsrfTokenInternal($token, $trueToken) + || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken); + } + + private function validateCsrfTokenInternal($token, $trueToken) + { + $token = base64_decode($token); + $n = StringHelper::byteLength($token); + if ($n <= self::CSRF_MASK_LENGTH) { + return false; + } + $mask = StringHelper::byteSubstr($token, 0, self::CSRF_MASK_LENGTH); + $token = StringHelper::byteSubstr($token, self::CSRF_MASK_LENGTH, $n - self::CSRF_MASK_LENGTH); + $token = $this->xorTokens($mask, $token); + return $token === $trueToken; } } diff --git a/framework/yii/web/View.php b/framework/yii/web/View.php index 790e4fd..f29a1e4 100644 --- a/framework/yii/web/View.php +++ b/framework/yii/web/View.php @@ -388,7 +388,7 @@ class View extends \yii\base\View $request = Yii::$app->getRequest(); if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) { $lines[] = Html::tag('meta', '', ['name' => 'csrf-var', 'content' => $request->csrfVar]); - $lines[] = Html::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getCsrfToken()]); + $lines[] = Html::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getMaskedCsrfToken()]); } if (!empty($this->linkTags)) {