Commit c8960168 by Qiang Xue

Fixes #1634: Use masked CSRF tokens to prevent BREACH exploits

parent 2686403c
...@@ -31,6 +31,7 @@ Yii Framework 2 Change Log ...@@ -31,6 +31,7 @@ Yii Framework 2 Change Log
- Enh #1581: Added `ActiveQuery::joinWith()` and `ActiveQuery::innerJoinWith()` to support joining with relations (qiangxue) - 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 #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight)
- Enh #1611: Added `BaseActiveRecord::markAttributeDirty()` (qiangxue) - Enh #1611: Added `BaseActiveRecord::markAttributeDirty()` (qiangxue)
- Enh #1634: Use masked CSRF tokens to prevent BREACH exploits (qiangxue)
- Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue) - Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue)
- Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
......
...@@ -241,7 +241,7 @@ class BaseHtml ...@@ -241,7 +241,7 @@ class BaseHtml
$method = 'post'; $method = 'post';
} }
if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) {
$hiddenInputs[] = static::hiddenInput($request->csrfVar, $request->getCsrfToken()); $hiddenInputs[] = static::hiddenInput($request->csrfVar, $request->getMaskedCsrfToken());
} }
} }
......
...@@ -10,6 +10,7 @@ namespace yii\web; ...@@ -10,6 +10,7 @@ namespace yii\web;
use Yii; use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\Security; use yii\helpers\Security;
use yii\helpers\StringHelper;
/** /**
* The web Request class represents an HTTP request * The web Request class represents an HTTP request
...@@ -83,6 +84,10 @@ class Request extends \yii\base\Request ...@@ -83,6 +84,10 @@ class Request extends \yii\base\Request
* The name of the HTTP header for sending CSRF token. * The name of the HTTP header for sending CSRF token.
*/ */
const CSRF_HEADER = 'X-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 ...@@ -1021,6 +1026,43 @@ class Request extends \yii\base\Request
return $this->_csrfCookie->value; 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. * @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 ...@@ -1072,6 +1114,20 @@ class Request extends \yii\base\Request
$token = $this->getPost($this->csrfVar); $token = $this->getPost($this->csrfVar);
break; 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;
} }
} }
...@@ -388,7 +388,7 @@ class View extends \yii\base\View ...@@ -388,7 +388,7 @@ class View extends \yii\base\View
$request = Yii::$app->getRequest(); $request = Yii::$app->getRequest();
if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) { if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) {
$lines[] = Html::tag('meta', '', ['name' => 'csrf-var', 'content' => $request->csrfVar]); $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)) { if (!empty($this->linkTags)) {
......
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