diff options
Diffstat (limited to 'lib/private')
-rw-r--r-- | lib/private/AppFramework/DependencyInjection/DIContainer.php | 3 | ||||
-rw-r--r-- | lib/private/AppFramework/Middleware/Security/CORSMiddleware.php | 25 | ||||
-rw-r--r-- | lib/private/IntegrityCheck/Checker.php | 24 | ||||
-rw-r--r-- | lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php | 5 | ||||
-rw-r--r-- | lib/private/Security/Bruteforce/Throttler.php | 230 | ||||
-rw-r--r-- | lib/private/Server.php | 16 | ||||
-rw-r--r-- | lib/private/User/Session.php | 26 | ||||
-rw-r--r-- | lib/private/legacy/api.php | 2 | ||||
-rw-r--r-- | lib/private/legacy/app.php | 6 | ||||
-rw-r--r-- | lib/private/legacy/defaults.php | 11 |
10 files changed, 315 insertions, 33 deletions
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 32a85606abf..893d6cb9aa6 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -352,7 +352,8 @@ class DIContainer extends SimpleContainer implements IAppContainer { return new CORSMiddleware( $c['Request'], $c['ControllerMethodReflector'], - $c['OCP\IUserSession'] + $c['OCP\IUserSession'], + $c->getServer()->getBruteForceThrottler() ); }); diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 32a507623e3..04de4bc92d3 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -27,6 +27,7 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Authentication\Exceptions\PasswordLoginForbiddenException; +use OC\Security\Bruteforce\Throttler; use OC\User\Session; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -42,33 +43,29 @@ use OCP\IRequest; * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS */ class CORSMiddleware extends Middleware { - - /** - * @var IRequest - */ + /** @var IRequest */ private $request; - - /** - * @var ControllerMethodReflector - */ + /** @var ControllerMethodReflector */ private $reflector; - - /** - * @var Session - */ + /** @var Session */ private $session; + /** @var Throttler */ + private $throttler; /** * @param IRequest $request * @param ControllerMethodReflector $reflector * @param Session $session + * @param Throttler $throttler */ public function __construct(IRequest $request, ControllerMethodReflector $reflector, - Session $session) { + Session $session, + Throttler $throttler) { $this->request = $request; $this->reflector = $reflector; $this->session = $session; + $this->throttler = $throttler; } /** @@ -91,7 +88,7 @@ class CORSMiddleware extends Middleware { $this->session->logout(); try { - if (!$this->session->logClientIn($user, $pass, $this->request)) { + if (!$this->session->logClientIn($user, $pass, $this->request, $this->throttler)) { throw new SecurityException('CORS requires basic auth', Http::STATUS_UNAUTHORIZED); } } catch (PasswordLoginForbiddenException $ex) { diff --git a/lib/private/IntegrityCheck/Checker.php b/lib/private/IntegrityCheck/Checker.php index 57127f280c4..d087720c11a 100644 --- a/lib/private/IntegrityCheck/Checker.php +++ b/lib/private/IntegrityCheck/Checker.php @@ -323,13 +323,20 @@ class Checker { $signature = base64_decode($signatureData['signature']); $certificate = $signatureData['certificate']; - // Check if certificate is signed by ownCloud Root Authority + // Check if certificate is signed by Nextcloud Root Authority $x509 = new \phpseclib\File\X509(); $rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt'); $x509->loadCA($rootCertificatePublicKey); $x509->loadX509($certificate); if(!$x509->validateSignature()) { - throw new InvalidSignatureException('Certificate is not valid.'); + // FIXME: Once Nextcloud has it's own appstore we should remove the ownCloud Root Authority from here + $x509 = new \phpseclib\File\X509(); + $rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/owncloud.crt'); + $x509->loadCA($rootCertificatePublicKey); + $x509->loadX509($certificate); + if(!$x509->validateSignature()) { + throw new InvalidSignatureException('Certificate is not valid.'); + } } // Verify if certificate has proper CN. "core" CN is always trusted. if($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') { @@ -347,6 +354,19 @@ class Checker { throw new InvalidSignatureException('Signature could not get verified.'); } + // Fixes for the updater as shipped with ownCloud 9.0.x: The updater is + // replaced after the code integrity check is performed. + // + // Due to this reason we exclude the whole updater/ folder from the code + // integrity check. + if($basePath === $this->environmentHelper->getServerRoot()) { + foreach($expectedHashes as $fileName => $hash) { + if(strpos($fileName, 'updater/') === 0) { + unset($expectedHashes[$fileName]); + } + } + } + // Compare the list of files which are not identical $currentInstanceHashes = $this->generateHashes($this->getFolderIterator($basePath), $basePath); $differencesA = array_diff($expectedHashes, $currentInstanceHashes); diff --git a/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php b/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php index c8db8022112..80a89e8c3a4 100644 --- a/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php +++ b/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php @@ -40,6 +40,11 @@ class ExcludeFoldersByPathFilterIterator extends \RecursiveFilterIterator { rtrim($root . '/apps', '/'), rtrim($root . '/assets', '/'), rtrim($root . '/lost+found', '/'), + // Ignore folders generated by updater since the updater is replaced + // after the integrity check is run. + // See https://github.com/owncloud/updater/issues/318#issuecomment-212497846 + rtrim($root . '/updater', '/'), + rtrim($root . '/_oc_upgrade', '/'), ]; $customDataDir = \OC::$server->getConfig()->getSystemValue('datadirectory', ''); if($customDataDir !== '') { diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php new file mode 100644 index 00000000000..0de7677285b --- /dev/null +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -0,0 +1,230 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Security\Bruteforce; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ILogger; + +/** + * Class Throttler implements the bruteforce protection for security actions in + * Nextcloud. + * + * It is working by logging invalid login attempts to the database and slowing + * down all login attempts from the same subnet. The max delay is 30 seconds and + * the starting delay are 200 milliseconds. (after the first failed login) + * + * This is based on Paragonie's AirBrake for Airship CMS. You can find the original + * code at https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/src/Engine/Security/AirBrake.php + * + * @package OC\Security\Bruteforce + */ +class Throttler { + const LOGIN_ACTION = 'login'; + + /** @var IDBConnection */ + private $db; + /** @var ITimeFactory */ + private $timeFactory; + /** @var ILogger */ + private $logger; + /** @var IConfig */ + private $config; + + /** + * @param IDBConnection $db + * @param ITimeFactory $timeFactory + * @param ILogger $logger + * @param IConfig $config + */ + public function __construct(IDBConnection $db, + ITimeFactory $timeFactory, + ILogger $logger, + IConfig $config) { + $this->db = $db; + $this->timeFactory = $timeFactory; + $this->logger = $logger; + $this->config = $config; + } + + /** + * Convert a number of seconds into the appropriate DateInterval + * + * @param int $expire + * @return \DateInterval + */ + private function getCutoff($expire) { + $d1 = new \DateTime(); + $d2 = clone $d1; + $d2->sub(new \DateInterval('PT' . $expire . 'S')); + return $d2->diff($d1); + } + + /** + * Return the given subnet for an IPv4 address and mask bits + * + * @param string $ip + * @param int $maskBits + * @return string + */ + private function getIPv4Subnet($ip, + $maskBits = 32) { + $binary = \inet_pton($ip); + for ($i = 32; $i > $maskBits; $i -= 8) { + $j = \intdiv($i, 8) - 1; + $k = (int) \min(8, $i - $maskBits); + $mask = (0xff - ((pow(2, $k)) - 1)); + $int = \unpack('C', $binary[$j]); + $binary[$j] = \pack('C', $int[1] & $mask); + } + return \inet_ntop($binary).'/'.$maskBits; + } + + /** + * Return the given subnet for an IPv6 address and mask bits + * + * @param string $ip + * @param int $maskBits + * @return string + */ + private function getIPv6Subnet($ip, $maskBits = 48) { + $binary = \inet_pton($ip); + for ($i = 128; $i > $maskBits; $i -= 8) { + $j = \intdiv($i, 8) - 1; + $k = (int) \min(8, $i - $maskBits); + $mask = (0xff - ((pow(2, $k)) - 1)); + $int = \unpack('C', $binary[$j]); + $binary[$j] = \pack('C', $int[1] & $mask); + } + return \inet_ntop($binary).'/'.$maskBits; + } + + /** + * Return the given subnet for an IP and the configured mask bits + * + * Determine if the IP is an IPv4 or IPv6 address, then pass to the correct + * method for handling that specific type. + * + * @param string $ip + * @return string + */ + private function getSubnet($ip) { + if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip)) { + return $this->getIPv4Subnet( + $ip, + 32 + ); + } + return $this->getIPv6Subnet( + $ip, + 128 + ); + } + + /** + * Register a failed attempt to bruteforce a security control + * + * @param string $action + * @param string $ip + * @param array $metadata Optional metadata logged to the database + */ + public function registerAttempt($action, + $ip, + array $metadata = []) { + // No need to log if the bruteforce protection is disabled + if($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) { + return; + } + + $values = [ + 'action' => $action, + 'occurred' => $this->timeFactory->getTime(), + 'ip' => $ip, + 'subnet' => $this->getSubnet($ip), + 'metadata' => json_encode($metadata), + ]; + + $this->logger->notice( + sprintf( + 'Bruteforce attempt from "%s" detected for action "%s".', + $ip, + $action + ), + [ + 'app' => 'core', + ] + ); + + $qb = $this->db->getQueryBuilder(); + $qb->insert('bruteforce_attempts'); + foreach($values as $column => $value) { + $qb->setValue($column, $qb->createNamedParameter($value)); + } + $qb->execute(); + } + + /** + * Get the throttling delay (in milliseconds) + * + * @param string $ip + * @return int + */ + public function getDelay($ip) { + $cutoffTime = (new \DateTime()) + ->sub($this->getCutoff(43200)) + ->getTimestamp(); + + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('bruteforce_attempts') + ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime))) + ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($this->getSubnet($ip)))); + $attempts = count($qb->execute()->fetchAll()); + + if ($attempts === 0) { + return 0; + } + + $maxDelay = 30; + $firstDelay = 0.1; + if ($attempts > (8 * PHP_INT_SIZE - 1)) { + // Don't ever overflow. Just assume the maxDelay time:s + $firstDelay = $maxDelay; + } else { + $firstDelay *= pow(2, $attempts); + if ($firstDelay > $maxDelay) { + $firstDelay = $maxDelay; + } + } + return (int) \ceil($firstDelay * 1000); + } + + /** + * Will sleep for the defined amount of time + * + * @param string $ip + */ + public function sleepDelay($ip) { + usleep($this->getDelay($ip) * 1000); + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index eb2c26415bc..6ffdeb9211e 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -66,6 +66,7 @@ use OC\Lock\NoopLockingProvider; use OC\Mail\Mailer; use OC\Memcache\ArrayCache; use OC\Notification\Manager; +use OC\Security\Bruteforce\Throttler; use OC\Security\CertificateManager; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\Crypto; @@ -503,6 +504,14 @@ class Server extends ServerContainer implements IServerContainer { $this->registerService('TrustedDomainHelper', function ($c) { return new TrustedDomainHelper($this->getConfig()); }); + $this->registerService('Throttler', function(Server $c) { + return new Throttler( + $c->getDatabaseConnection(), + new TimeFactory(), + $c->getLogger(), + $c->getConfig() + ); + }); $this->registerService('IntegrityCodeChecker', function (Server $c) { // IConfig and IAppManager requires a working database. This code // might however be called when ownCloud is not yet setup. @@ -1331,6 +1340,13 @@ class Server extends ServerContainer implements IServerContainer { } /** + * @return Throttler + */ + public function getBruteForceThrottler() { + return $this->query('Throttler'); + } + + /** * @return IContentSecurityPolicyManager */ public function getContentSecurityPolicyManager() { diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index dcc2e66c6c3..8d12982dd1a 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -95,7 +95,11 @@ class Session implements IUserSession, Emitter { * @param IProvider $tokenProvider * @param IConfig $config */ - public function __construct(IUserManager $manager, ISession $session, ITimeFactory $timeFacory, $tokenProvider, IConfig $config) { + public function __construct(IUserManager $manager, + ISession $session, + ITimeFactory $timeFacory, + $tokenProvider, + IConfig $config) { $this->manager = $manager; $this->session = $session; $this->timeFacory = $timeFacory; @@ -280,7 +284,6 @@ class Session implements IUserSession, Emitter { */ public function login($uid, $password) { $this->session->regenerateId(); - if ($this->validateToken($password, $uid)) { return $this->loginWithToken($password); } else { @@ -298,11 +301,18 @@ class Session implements IUserSession, Emitter { * @param string $user * @param string $password * @param IRequest $request + * @param OC\Security\Bruteforce\Throttler $throttler * @throws LoginException * @throws PasswordLoginForbiddenException * @return boolean */ - public function logClientIn($user, $password, IRequest $request) { + public function logClientIn($user, + $password, + IRequest $request, + OC\Security\Bruteforce\Throttler $throttler) { + $currentDelay = $throttler->getDelay($request->getRemoteAddress()); + $throttler->sleepDelay($request->getRemoteAddress()); + $isTokenPassword = $this->isTokenPassword($password); if (!$isTokenPassword && $this->isTokenAuthEnforced()) { throw new PasswordLoginForbiddenException(); @@ -315,6 +325,11 @@ class Session implements IUserSession, Emitter { if (count($users) === 1) { return $this->login($users[0]->getUID(), $password); } + + $throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]); + if($currentDelay === 0) { + $throttler->sleepDelay($request->getRemoteAddress()); + } return false; } @@ -391,10 +406,11 @@ class Session implements IUserSession, Emitter { * @param IRequest $request * @return boolean if the login was successful */ - public function tryBasicAuthLogin(IRequest $request) { + public function tryBasicAuthLogin(IRequest $request, + OC\Security\Bruteforce\Throttler $throttler) { if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) { try { - if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request)) { + if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) { /** * Add DAV authenticated. This should in an ideal world not be * necessary but the iOS App reads cookies from anywhere instead diff --git a/lib/private/legacy/api.php b/lib/private/legacy/api.php index 024f3c0fb63..88eb7b09a78 100644 --- a/lib/private/legacy/api.php +++ b/lib/private/legacy/api.php @@ -364,7 +364,7 @@ class OC_API { try { $loginSuccess = $userSession->tryTokenLogin($request); if (!$loginSuccess) { - $loginSuccess = $userSession->tryBasicAuthLogin($request); + $loginSuccess = $userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler()); } } catch (\OC\User\LoginException $e) { return false; diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index 37ceea35ac0..9753e2efd50 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -421,9 +421,7 @@ class OC_App { $settings = array(); // by default, settings only contain the help menu - /* - * FIXME: Add help sidebar back once documentation is properly branded. - if (OC_Util::getEditionString() === '' && + if (OC_Util::getEditionString() === '' && \OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true ) { $settings = array( @@ -435,7 +433,7 @@ class OC_App { "icon" => $urlGenerator->imagePath("settings", "help.svg") ) ); - }*/ + } // if the user is logged-in if (OC_User::isLoggedIn()) { diff --git a/lib/private/legacy/defaults.php b/lib/private/legacy/defaults.php index 2a97cfe89ed..58aa4e11061 100644 --- a/lib/private/legacy/defaults.php +++ b/lib/private/legacy/defaults.php @@ -49,18 +49,17 @@ class OC_Defaults { function __construct() { $this->l = \OC::$server->getL10N('lib'); - $version = \OCP\Util::getVersion(); $this->defaultEntity = 'Nextcloud'; /* e.g. company name, used for footers and copyright notices */ $this->defaultName = 'Nextcloud'; /* short name, used when referring to the software */ $this->defaultTitle = 'Nextcloud'; /* can be a longer name, for titles */ $this->defaultBaseUrl = 'https://nextcloud.com'; $this->defaultSyncClientUrl = 'https://nextcloud.com/install'; - $this->defaultiOSClientUrl = 'https://itunes.apple.com/us/app/owncloud/id543672169?mt=8'; - $this->defaultiTunesAppId = '543672169'; - $this->defaultAndroidClientUrl = 'https://play.google.com/store/apps/details?id=com.owncloud.android'; - $this->defaultDocBaseUrl = 'https://doc.owncloud.org'; - $this->defaultDocVersion = $version[0] . '.' . $version[1]; // used to generate doc links + $this->defaultiOSClientUrl = 'https://itunes.apple.com/us/app/nextcloud/id1125420102?mt=8'; + $this->defaultiTunesAppId = '1125420102'; + $this->defaultAndroidClientUrl = 'https://play.google.com/store/apps/details?id=com.nextcloud.client'; + $this->defaultDocBaseUrl = 'https://docs.nextcloud.org'; + $this->defaultDocVersion = '10'; // used to generate doc links $this->defaultSlogan = $this->l->t('a safe home for all your data'); $this->defaultLogoClaim = ''; $this->defaultMailHeaderColor = '#0082c9'; /* header color of mail notifications */ |