diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 2b944c116e..caddb5a235 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -33,6 +33,8 @@ namespace OC\AppFramework\Http; +use OC\Security\CSRF\CsrfToken; +use OC\Security\CSRF\CsrfTokenManager; use OC\Security\TrustedDomainHelper; use OCP\IConfig; use OCP\IRequest; @@ -75,6 +77,8 @@ class Request implements \ArrayAccess, \Countable, IRequest { protected $requestId = ''; /** @var ICrypto */ protected $crypto; + /** @var CsrfTokenManager|null */ + protected $csrfTokenManager; /** @var bool */ protected $contentDecoded = false; @@ -92,17 +96,20 @@ class Request implements \ArrayAccess, \Countable, IRequest { * - string|false 'requesttoken' the requesttoken or false when not available * @param ISecureRandom $secureRandom * @param IConfig $config + * @param CsrfTokenManager|null $csrfTokenManager * @param string $stream * @see http://www.php.net/manual/en/reserved.variables.php */ public function __construct(array $vars=array(), ISecureRandom $secureRandom = null, IConfig $config, - $stream='php://input') { + CsrfTokenManager $csrfTokenManager = null, + $stream = 'php://input') { $this->inputStream = $stream; $this->items['params'] = array(); $this->secureRandom = $secureRandom; $this->config = $config; + $this->csrfTokenManager = $csrfTokenManager; if(!array_key_exists('method', $vars)) { $vars['method'] = 'GET'; @@ -421,10 +428,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Checks if the CSRF check was correct * @return bool true if CSRF check passed - * @see OC_Util::callRegister() */ public function passesCSRFCheck() { - if($this->items['requesttoken'] === false) { + if($this->csrfTokenManager === null) { return false; } @@ -438,23 +444,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { //no token found. return false; } + $token = new CsrfToken($token); - // Deobfuscate token to prevent BREACH like attacks - $token = explode(':', $token); - if (count($token) !== 2) { - return false; - } - - $obfuscatedToken = $token[0]; - $secret = $token[1]; - $deobfuscatedToken = base64_decode($obfuscatedToken) ^ $secret; - - // Check if the token is valid - if(hash_equals($deobfuscatedToken, $this->items['requesttoken'])) { - return true; - } else { - return false; - } + return $this->csrfTokenManager->isTokenValid($token); } /** diff --git a/lib/private/security/csrf/csrftoken.php b/lib/private/security/csrf/csrftoken.php new file mode 100644 index 0000000000..4524d0db6e --- /dev/null +++ b/lib/private/security/csrf/csrftoken.php @@ -0,0 +1,69 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Security\CSRF; + +/** + * Class CsrfToken represents the stored or provided CSRF token. To mitigate + * BREACH alike vulnerabilities the token is returned in an encrypted value as + * well in an unencrypted value. For display measures to the user always the + * unencrypted one should be chosen. + * + * @package OC\Security\CSRF + */ +class CsrfToken { + /** @var string */ + private $value; + + /** + * @param string $value Value of the token. Can be encrypted or not encrypted. + */ + public function __construct($value) { + $this->value = $value; + } + + /** + * Encrypted value of the token. This is used to mitigate BREACH alike + * vulnerabilities. For display measures do use this functionality. + * + * @return string + */ + public function getEncryptedValue() { + $sharedSecret = base64_encode(random_bytes(strlen($this->value))); + return base64_encode($this->value ^ $sharedSecret) .':'.$sharedSecret; + } + + /** + * The unencrypted value of the token. Used for decrypting an already + * encrypted token. + * + * @return int + */ + public function getDecryptedValue() { + $token = explode(':', $this->value); + if (count($token) !== 2) { + return ''; + } + $obfuscatedToken = $token[0]; + $secret = $token[1]; + return base64_decode($obfuscatedToken) ^ $secret; + } +} diff --git a/lib/private/security/csrf/csrftokengenerator.php b/lib/private/security/csrf/csrftokengenerator.php new file mode 100644 index 0000000000..6ea71636d2 --- /dev/null +++ b/lib/private/security/csrf/csrftokengenerator.php @@ -0,0 +1,52 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Security\CSRF; + +use OCP\Security\ISecureRandom; + +/** + * Class CsrfTokenGenerator is used to generate a cryptographically secure + * pseudo-random number for the token. + * + * @package OC\Security\CSRF + */ +class CsrfTokenGenerator { + /** @var ISecureRandom */ + private $random; + + /** + * @param ISecureRandom $random + */ + public function __construct(ISecureRandom $random) { + $this->random = $random; + } + + /** + * Generate a new CSRF token. + * + * @param int $length Length of the token in characters. + * @return string + */ + public function generateToken($length = 32) { + return $this->random->generate($length); + } +} diff --git a/lib/private/security/csrf/csrftokenmanager.php b/lib/private/security/csrf/csrftokenmanager.php new file mode 100644 index 0000000000..8d1bf5c081 --- /dev/null +++ b/lib/private/security/csrf/csrftokenmanager.php @@ -0,0 +1,97 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Security\CSRF; + +use OC\Security\CSRF\TokenStorage\SessionStorage; + +/** + * Class CsrfTokenManager is the manager for all CSRF token related activities. + * + * @package OC\Security\CSRF + */ +class CsrfTokenManager { + /** @var CsrfTokenGenerator */ + private $tokenGenerator; + /** @var SessionStorage */ + private $sessionStorage; + + /** + * @param CsrfTokenGenerator $tokenGenerator + * @param SessionStorage $storageInterface + */ + public function __construct(CsrfTokenGenerator $tokenGenerator, + SessionStorage $storageInterface) { + $this->tokenGenerator = $tokenGenerator; + $this->sessionStorage = $storageInterface; + } + + /** + * Returns the current CSRF token, if none set it will create a new one. + * + * @return CsrfToken + */ + public function getToken() { + if($this->sessionStorage->hasToken()) { + $value = $this->sessionStorage->getToken(); + } else { + $value = $this->tokenGenerator->generateToken(); + $this->sessionStorage->setToken($value); + } + + return new CsrfToken($value); + } + + /** + * Invalidates any current token and sets a new one. + * + * @return CsrfToken + */ + public function refreshToken() { + $value = $this->tokenGenerator->generateToken(); + $this->sessionStorage->setToken($value); + return new CsrfToken($value); + } + + /** + * Remove the current token from the storage. + */ + public function removeToken() { + $this->sessionStorage->removeToken(); + } + + /** + * Verifies whether the provided token is valid. + * + * @param CsrfToken $token + * @return bool + */ + public function isTokenValid(CsrfToken $token) { + if(!$this->sessionStorage->hasToken()) { + return false; + } + + return hash_equals( + $this->sessionStorage->getToken(), + $token->getDecryptedValue() + ); + } +} diff --git a/lib/private/security/csrf/tokenstorage/sessionstorage.php b/lib/private/security/csrf/tokenstorage/sessionstorage.php new file mode 100644 index 0000000000..e1c8c96e92 --- /dev/null +++ b/lib/private/security/csrf/tokenstorage/sessionstorage.php @@ -0,0 +1,80 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Security\CSRF\TokenStorage; + +use OCP\ISession; + +/** + * Class SessionStorage provides the session storage + * + * @package OC\Security\CSRF\TokenStorage + */ +class SessionStorage { + /** @var ISession */ + private $session; + + /** + * @param ISession $session + */ + public function __construct(ISession $session) { + $this->session = $session; + } + + /** + * Returns the current token or throws an exception if none is found. + * + * @return string + * @throws \Exception + */ + public function getToken() { + $token = $this->session->get('requesttoken'); + if(empty($token)) { + throw new \Exception('Session does not contain a requesttoken'); + } + + return $token; + } + + /** + * Set the valid current token to $value. + * + * @param string $value + */ + public function setToken($value) { + $this->session->set('requesttoken', $value); + } + + /** + * Removes the current token. + */ + public function removeToken() { + $this->session->remove('requesttoken'); + } + /** + * Whether the storage has a storage. + * + * @return bool + */ + public function hasToken() { + return $this->session->exists('requesttoken'); + } +} diff --git a/lib/private/server.php b/lib/private/server.php index 6e9c5ca0c6..eca7ac348e 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -64,6 +64,9 @@ use OC\Mail\Mailer; use OC\Notification\Manager; use OC\Security\CertificateManager; use OC\Security\Crypto; +use OC\Security\CSRF\CsrfTokenGenerator; +use OC\Security\CSRF\CsrfTokenManager; +use OC\Security\CSRF\TokenStorage\SessionStorage; use OC\Security\Hasher; use OC\Security\CredentialsManager; use OC\Security\SecureRandom; @@ -469,12 +472,6 @@ class Server extends ServerContainer implements IServerContainer { $urlParams = []; } - if ($this->getSession()->exists('requesttoken')) { - $requestToken = $this->getSession()->get('requesttoken'); - } else { - $requestToken = false; - } - if (defined('PHPUNIT_RUN') && PHPUNIT_RUN && in_array('fakeinput', stream_get_wrappers()) ) { @@ -495,10 +492,10 @@ class Server extends ServerContainer implements IServerContainer { ? $_SERVER['REQUEST_METHOD'] : null, 'urlParams' => $urlParams, - 'requesttoken' => $requestToken, ], $this->getSecureRandom(), $this->getConfig(), + $this->getCsrfTokenManager(), $stream ); }); @@ -588,6 +585,15 @@ class Server extends ServerContainer implements IServerContainer { $request ); }); + $this->registerService('CsrfTokenManager', function (Server $c) { + $tokenGenerator = new CsrfTokenGenerator($c->getSecureRandom()); + $sessionStorage = new SessionStorage($c->getSession()); + + return new CsrfTokenManager( + $tokenGenerator, + $sessionStorage + ); + }); $this->registerService('ShareManager', function(Server $c) { $config = $c->getConfig(); $factoryClass = $config->getSystemValue('sharing.managerFactory', '\OC\Share20\ProviderFactory'); @@ -1204,6 +1210,13 @@ class Server extends ServerContainer implements IServerContainer { return $this->query('CryptoWrapper'); } + /** + * @return CsrfTokenManager + */ + public function getCsrfTokenManager() { + return $this->query('CsrfTokenManager'); + } + /** * Not a public API as of 8.2, wait for 9.0 * diff --git a/lib/private/template.php b/lib/private/template.php index 717f91a703..ae3e857a79 100644 --- a/lib/private/template.php +++ b/lib/private/template.php @@ -76,7 +76,7 @@ class OC_Template extends \OC\Template\Base { $theme = OC_Util::getTheme(); - $requesttoken = (OC::$server->getSession() and $registerCall) ? OC_Util::callRegister() : ''; + $requestToken = (OC::$server->getSession() && $registerCall) ? \OCP\Util::callRegister() : ''; $parts = explode('/', $app); // fix translation when app is something like core/lostpassword $l10n = \OC::$server->getL10N($parts[0]); @@ -89,7 +89,7 @@ class OC_Template extends \OC\Template\Base { $this->path = $path; $this->app = $app; - parent::__construct($template, $requesttoken, $l10n, $themeDefaults); + parent::__construct($template, $requestToken, $l10n, $themeDefaults); } public static function initTemplateEngine($renderAs) { diff --git a/lib/private/template/base.php b/lib/private/template/base.php index 944747197b..938cca4c38 100644 --- a/lib/private/template/base.php +++ b/lib/private/template/base.php @@ -34,12 +34,13 @@ class Base { /** * @param string $template + * @param string $requestToken * @param \OC_L10N $l10n * @param \OC_Defaults $theme */ - public function __construct( $template, $requesttoken, $l10n, $theme ) { + public function __construct($template, $requestToken, $l10n, $theme ) { $this->vars = array(); - $this->vars['requesttoken'] = $requesttoken; + $this->vars['requesttoken'] = $requestToken; $this->l10n = $l10n; $this->template = $template; $this->theme = $theme; diff --git a/lib/private/user.php b/lib/private/user.php index 7d1f21cc40..90925a2c89 100644 --- a/lib/private/user.php +++ b/lib/private/user.php @@ -328,7 +328,7 @@ class OC_User { return $backend->getLogoutAttribute(); } - return 'href="' . link_to('', 'index.php') . '?logout=true&requesttoken=' . urlencode(OC_Util::callRegister()) . '"'; + return 'href="' . link_to('', 'index.php') . '?logout=true&requesttoken=' . urlencode(\OCP\Util::callRegister()) . '"'; } /** diff --git a/lib/private/util.php b/lib/private/util.php index 5ae5c452d6..64695d95a0 100644 --- a/lib/private/util.php +++ b/lib/private/util.php @@ -1105,42 +1105,6 @@ class OC_Util { return $id; } - protected static $obfuscatedToken; - /** - * Register an get/post call. Important to prevent CSRF attacks. - * - * @return string The encrypted CSRF token, the shared secret is appended after the `:`. - * - * @description - * Creates a 'request token' (random) and stores it inside the session. - * Ever subsequent (ajax) request must use such a valid token to succeed, - * otherwise the request will be denied as a protection against CSRF. - */ - public static function callRegister() { - // Use existing token if function has already been called - if(isset(self::$obfuscatedToken)) { - return self::$obfuscatedToken; - } - - $tokenLength = 30; - - // Check if a token exists - if (!\OC::$server->getSession()->exists('requesttoken')) { - // No valid token found, generate a new one. - $requestToken = \OC::$server->getSecureRandom()->generate($tokenLength); - \OC::$server->getSession()->set('requesttoken', $requestToken); - } else { - // Valid token already exists, send it - $requestToken = \OC::$server->getSession()->get('requesttoken'); - } - - // XOR the token to mitigate breach-like attacks - $sharedSecret = \OC::$server->getSecureRandom()->generate($tokenLength); - self::$obfuscatedToken = base64_encode($requestToken ^ $sharedSecret) .':'.$sharedSecret; - - return self::$obfuscatedToken; - } - /** * Public function to sanitize HTML * diff --git a/lib/public/util.php b/lib/public/util.php index 4762f595c2..45df62ac73 100644 --- a/lib/public/util.php +++ b/lib/public/util.php @@ -479,19 +479,29 @@ class Util { return(\OC_Hook::emit( $signalclass, $signalname, $params )); } + /** + * Cached encrypted CSRF token. Some static unit-tests of ownCloud compare + * multiple OC_Template elements which invoke `callRegister`. If the value + * would not be cached these unit-tests would fail. + * @var string + */ + private static $token = ''; + /** * Register an get/post call. This is important to prevent CSRF attacks - * TODO: write example * @since 4.5.0 */ public static function callRegister() { - return(\OC_Util::callRegister()); + if(self::$token === '') { + self::$token = \OC::$server->getCsrfTokenManager()->getToken()->getEncryptedValue(); + } + return self::$token; } /** * Check an ajax get/post call if the request token is valid. exit if not. - * Todo: Write howto * @since 4.5.0 + * @deprecated 9.0.0 Use annotations based on the app framework. */ public static function callCheck() { if (!(\OC::$server->getRequest()->passesCSRFCheck())) { diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index ab79eb498f..3f1d09c2a9 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -10,6 +10,8 @@ namespace OC\AppFramework\Http; +use OC\Security\CSRF\CsrfToken; +use OC\Security\CSRF\CsrfTokenManager; use OCP\Security\ISecureRandom; use OCP\IConfig; @@ -25,6 +27,8 @@ class RequestTest extends \Test\TestCase { protected $secureRandom; /** @var IConfig */ protected $config; + /** @var CsrfTokenManager */ + protected $csrfTokenManager; protected function setUp() { parent::setUp(); @@ -37,6 +41,8 @@ class RequestTest extends \Test\TestCase { $this->secureRandom = $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(); $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock(); + $this->csrfTokenManager = $this->getMockBuilder('\OC\Security\CSRF\CsrfTokenManager') + ->disableOriginalConstructor()->getMock(); } protected function tearDown() { @@ -54,6 +60,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -86,6 +93,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -108,6 +116,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -127,6 +136,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -146,6 +156,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -162,6 +173,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -183,6 +195,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -206,6 +219,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -227,6 +241,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -251,6 +266,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -271,6 +287,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -295,6 +312,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -324,6 +342,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -345,6 +364,7 @@ class RequestTest extends \Test\TestCase { $vars, $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -361,6 +381,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -372,6 +393,7 @@ class RequestTest extends \Test\TestCase { [], \OC::$server->getSecureRandom(), $this->config, + $this->csrfTokenManager, $this->stream ); $firstId = $request->getId(); @@ -396,6 +418,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -424,6 +447,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -452,6 +476,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -484,6 +509,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -534,6 +560,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -561,6 +588,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -582,6 +610,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); $requestHttp = new Request( @@ -592,6 +621,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -615,6 +645,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); $this->assertSame('https', $request->getServerProtocol()); @@ -635,6 +666,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); $this->assertSame('http', $request->getServerProtocol()); @@ -655,6 +687,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); $this->assertSame('http', $request->getServerProtocol()); @@ -671,6 +704,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); $this->assertSame('http', $request->getServerProtocol()); @@ -691,6 +725,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -712,6 +747,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -729,6 +765,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -817,6 +854,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -833,6 +871,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -850,6 +889,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -867,6 +907,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -894,6 +935,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -915,6 +957,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -941,6 +984,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -967,6 +1011,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -983,6 +1028,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1010,6 +1056,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1025,6 +1072,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1045,6 +1093,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1065,6 +1114,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1087,6 +1137,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1109,6 +1160,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1131,6 +1183,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1153,6 +1206,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1207,6 +1261,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ); @@ -1246,6 +1301,7 @@ class RequestTest extends \Test\TestCase { ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ]) ->getMock(); @@ -1266,13 +1322,19 @@ class RequestTest extends \Test\TestCase { 'get' => [ 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', ], - 'requesttoken' => 'MyStoredRequestToken', ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ]) ->getMock(); + $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->with($token) + ->willReturn(true); $this->assertTrue($request->passesCSRFCheck()); } @@ -1286,13 +1348,19 @@ class RequestTest extends \Test\TestCase { 'post' => [ 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', ], - 'requesttoken' => 'MyStoredRequestToken', ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ]) ->getMock(); + $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->with($token) + ->willReturn(true); $this->assertTrue($request->passesCSRFCheck()); } @@ -1306,13 +1374,19 @@ class RequestTest extends \Test\TestCase { 'server' => [ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', ], - 'requesttoken' => 'MyStoredRequestToken', ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ]) ->getMock(); + $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); + $this->csrfTokenManager + ->expects($this->once()) + ->method('isTokenValid') + ->with($token) + ->willReturn(true); $this->assertTrue($request->passesCSRFCheck()); } @@ -1342,14 +1416,21 @@ class RequestTest extends \Test\TestCase { 'server' => [ 'HTTP_REQUESTTOKEN' => $invalidToken, ], - 'requesttoken' => 'MyStoredRequestToken', ], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ]) ->getMock(); + $token = new CsrfToken($invalidToken); + $this->csrfTokenManager + ->expects($this->any()) + ->method('isTokenValid') + ->with($token) + ->willReturn(false); + $this->assertFalse($request->passesCSRFCheck()); } @@ -1361,6 +1442,7 @@ class RequestTest extends \Test\TestCase { [], $this->secureRandom, $this->config, + $this->csrfTokenManager, $this->stream ]) ->getMock(); diff --git a/tests/lib/security/csrf/CsrfTokenGeneratorTest.php b/tests/lib/security/csrf/CsrfTokenGeneratorTest.php new file mode 100644 index 0000000000..be7434f514 --- /dev/null +++ b/tests/lib/security/csrf/CsrfTokenGeneratorTest.php @@ -0,0 +1,54 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +class CsrfTokenGeneratorTest extends \Test\TestCase { + /** @var \OCP\Security\ISecureRandom */ + private $random; + /** @var \OC\Security\CSRF\CsrfTokenGenerator */ + private $csrfTokenGenerator; + + public function setUp() { + parent::setUp(); + $this->random = $this->getMockBuilder('\OCP\Security\ISecureRandom') + ->disableOriginalConstructor()->getMock(); + $this->csrfTokenGenerator = new \OC\Security\CSRF\CsrfTokenGenerator($this->random); + + } + + public function testGenerateTokenWithCustomNumber() { + $this->random + ->expects($this->once()) + ->method('generate') + ->with(3) + ->willReturn('abc'); + $this->assertSame('abc', $this->csrfTokenGenerator->generateToken(3)); + } + + public function testGenerateTokenWithDefault() { + $this->random + ->expects($this->once()) + ->method('generate') + ->with(32) + ->willReturn('12345678901234567890123456789012'); + $this->assertSame('12345678901234567890123456789012', $this->csrfTokenGenerator->generateToken(32)); + } +} + diff --git a/tests/lib/security/csrf/CsrfTokenManagerTest.php b/tests/lib/security/csrf/CsrfTokenManagerTest.php new file mode 100644 index 0000000000..145fc03c51 --- /dev/null +++ b/tests/lib/security/csrf/CsrfTokenManagerTest.php @@ -0,0 +1,134 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +class CsrfTokenManagerTest extends \Test\TestCase { + /** @var \OC\Security\CSRF\CsrfTokenManager */ + private $csrfTokenManager; + /** @var \OC\Security\CSRF\CsrfTokenGenerator */ + private $tokenGenerator; + /** @var \OC\Security\CSRF\TokenStorage\SessionStorage */ + private $storageInterface; + + public function setUp() { + parent::setUp(); + $this->tokenGenerator = $this->getMockBuilder('\OC\Security\CSRF\CsrfTokenGenerator') + ->disableOriginalConstructor()->getMock(); + $this->storageInterface = $this->getMockBuilder('\OC\Security\CSRF\TokenStorage\SessionStorage') + ->disableOriginalConstructor()->getMock(); + + $this->csrfTokenManager = new \OC\Security\CSRF\CsrfTokenManager( + $this->tokenGenerator, + $this->storageInterface + ); + } + + public function testGetTokenWithExistingToken() { + $this->storageInterface + ->expects($this->once()) + ->method('hasToken') + ->willReturn(true); + $this->storageInterface + ->expects($this->once()) + ->method('getToken') + ->willReturn('MyExistingToken'); + + $expected = new \OC\Security\CSRF\CsrfToken('MyExistingToken'); + $this->assertEquals($expected, $this->csrfTokenManager->getToken()); + } + + public function testGetTokenWithoutExistingToken() { + $this->storageInterface + ->expects($this->once()) + ->method('hasToken') + ->willReturn(false); + $this->tokenGenerator + ->expects($this->once()) + ->method('generateToken') + ->willReturn('MyNewToken'); + $this->storageInterface + ->expects($this->once()) + ->method('setToken') + ->with('MyNewToken'); + + $expected = new \OC\Security\CSRF\CsrfToken('MyNewToken'); + $this->assertEquals($expected, $this->csrfTokenManager->getToken()); + } + + public function testRefreshToken() { + $this->tokenGenerator + ->expects($this->once()) + ->method('generateToken') + ->willReturn('MyNewToken'); + $this->storageInterface + ->expects($this->once()) + ->method('setToken') + ->with('MyNewToken'); + + $expected = new \OC\Security\CSRF\CsrfToken('MyNewToken'); + $this->assertEquals($expected, $this->csrfTokenManager->refreshToken()); + } + + public function testRemoveToken() { + $this->storageInterface + ->expects($this->once()) + ->method('removeToken'); + + $this->csrfTokenManager->removeToken(); + } + + public function testIsTokenValidWithoutToken() { + $this->storageInterface + ->expects($this->once()) + ->method('hasToken') + ->willReturn(false); + $token = new \OC\Security\CSRF\CsrfToken('Token'); + + $this->assertSame(false, $this->csrfTokenManager->isTokenValid($token)); + } + + public function testIsTokenValidWithWrongToken() { + $this->storageInterface + ->expects($this->once()) + ->method('hasToken') + ->willReturn(true); + $token = new \OC\Security\CSRF\CsrfToken('Token'); + $this->storageInterface + ->expects($this->once()) + ->method('getToken') + ->willReturn('MyToken'); + + $this->assertSame(false, $this->csrfTokenManager->isTokenValid($token)); + } + + public function testIsTokenValidWithValidToken() { + $this->storageInterface + ->expects($this->once()) + ->method('hasToken') + ->willReturn(true); + $token = new \OC\Security\CSRF\CsrfToken('XlQhHjgWCgBXAEI0Khl+IQEiCXN2LUcDHAQTQAc1HQs=:qgkUlg8l3m8WnkOG4XM9Az33pAt1vSVMx4hcJFsxdqc='); + $this->storageInterface + ->expects($this->once()) + ->method('getToken') + ->willReturn('/3JKTq2ldmzcDr1f5zDJ7Wt0lEgqqfKF'); + + $this->assertSame(true, $this->csrfTokenManager->isTokenValid($token)); + } +} diff --git a/tests/lib/security/csrf/CsrfTokenTest.php b/tests/lib/security/csrf/CsrfTokenTest.php new file mode 100644 index 0000000000..62e6ad112e --- /dev/null +++ b/tests/lib/security/csrf/CsrfTokenTest.php @@ -0,0 +1,33 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +class CsrfTokenTest extends \Test\TestCase { + public function testGetEncryptedValue() { + $csrfToken = new \OC\Security\CSRF\CsrfToken('MyCsrfToken'); + $this->assertSame(33, strlen($csrfToken->getEncryptedValue())); + $this->assertSame(':', $csrfToken->getEncryptedValue()[16]); + } + + public function testGetDecryptedValue() { + $csrfToken = new \OC\Security\CSRF\CsrfToken('XlQhHjgWCgBXAEI0Khl+IQEiCXN2LUcDHAQTQAc1HQs=:qgkUlg8l3m8WnkOG4XM9Az33pAt1vSVMx4hcJFsxdqc='); + $this->assertSame('/3JKTq2ldmzcDr1f5zDJ7Wt0lEgqqfKF', $csrfToken->getDecryptedValue()); + } +} diff --git a/tests/lib/security/csrf/tokenstorage/SessionStorageTest.php b/tests/lib/security/csrf/tokenstorage/SessionStorageTest.php new file mode 100644 index 0000000000..3a83f6a8c0 --- /dev/null +++ b/tests/lib/security/csrf/tokenstorage/SessionStorageTest.php @@ -0,0 +1,107 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +class SessionStorageTest extends \Test\TestCase { + /** @var \OCP\ISession */ + private $session; + /** @var \OC\Security\CSRF\TokenStorage\SessionStorage */ + private $sessionStorage; + + public function setUp() { + parent::setUp(); + $this->session = $this->getMockBuilder('\OCP\ISession') + ->disableOriginalConstructor()->getMock(); + $this->sessionStorage = new \OC\Security\CSRF\TokenStorage\SessionStorage($this->session); + } + + /** + * @return array + */ + public function getTokenDataProvider() { + return [ + [ + '', + ], + [ + null, + ], + ]; + } + + /** + * @param string $token + * @dataProvider getTokenDataProvider + * + * @expectedException \Exception + * @expectedExceptionMessage Session does not contain a requesttoken + */ + public function testGetTokenWithEmptyToken($token) { + $this->session + ->expects($this->once()) + ->method('get') + ->with('requesttoken') + ->willReturn($token); + $this->sessionStorage->getToken(); + } + + public function testGetTokenWithValidToken() { + $this->session + ->expects($this->once()) + ->method('get') + ->with('requesttoken') + ->willReturn('MyFancyCsrfToken'); + $this->assertSame('MyFancyCsrfToken', $this->sessionStorage->getToken()); + } + + public function testSetToken() { + $this->session + ->expects($this->once()) + ->method('set') + ->with('requesttoken', 'TokenToSet'); + $this->sessionStorage->setToken('TokenToSet'); + } + + public function testRemoveToken() { + $this->session + ->expects($this->once()) + ->method('remove') + ->with('requesttoken'); + $this->sessionStorage->removeToken(); + } + + public function testHasTokenWithExistingToken() { + $this->session + ->expects($this->once()) + ->method('exists') + ->with('requesttoken') + ->willReturn(true); + $this->assertSame(true, $this->sessionStorage->hasToken()); + } + + public function testHasTokenWithoutExistingToken() { + $this->session + ->expects($this->once()) + ->method('exists') + ->with('requesttoken') + ->willReturn(false); + $this->assertSame(false, $this->sessionStorage->hasToken()); + } +} diff --git a/tests/lib/util.php b/tests/lib/util.php index f05a33766b..7880d56f63 100644 --- a/tests/lib/util.php +++ b/tests/lib/util.php @@ -89,11 +89,6 @@ class Test_Util extends \Test\TestCase { }); } - function testCallRegister() { - $result = strlen(OC_Util::callRegister()); - $this->assertEquals(71, $result); - } - function testSanitizeHTML() { $badArray = [ 'While it is unusual to pass an array',