diff --git a/index.php b/index.php index c58c40f5..4a133353 100644 --- a/index.php +++ b/index.php @@ -31,6 +31,97 @@ public function getData() { } } +class Auth { + /** @var Updater */ + private $updater; + /** @var string */ + private $password; + + /** + * @param Updater $updater + * @param string $password + */ + public function __construct(Updater $updater, + $password) { + $this->updater = $updater; + $this->password = $password; + } + /** + * Compares two strings. + * + * This method implements a constant-time algorithm to compare strings. + * Regardless of the used implementation, it will leak length information. + * + * @param string $knownString The string of known length to compare against + * @param string $userInput The string that the user can control + * + * @return bool true if the two strings are the same, false otherwise + * @license MIT + * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php + */ + private static function equals($knownString, $userInput) { + // Avoid making unnecessary duplications of secret data + if (!is_string($knownString)) { + $knownString = (string) $knownString; + } + if (!is_string($userInput)) { + $userInput = (string) $userInput; + } + if (function_exists('hash_equals')) { + return hash_equals($knownString, $userInput); + } + $knownLen = self::safeStrlen($knownString); + $userLen = self::safeStrlen($userInput); + if ($userLen !== $knownLen) { + return false; + } + $result = 0; + for ($i = 0; $i < $knownLen; ++$i) { + $result |= (ord($knownString[$i]) ^ ord($userInput[$i])); + } + // They are only identical strings if $result is exactly 0... + return 0 === $result; + } + /** + * Returns the number of bytes in a string. + * + * @param string $string The string whose length we wish to obtain + * + * @return int + * @license MIT + * @source https://github.com/symfony/security-core/blob/56721d5f5f63da7e08d05aa7668a5a9ef2367e1e/Util/StringUtils.php + */ + private static function safeStrlen($string) { + // Premature optimization + // Since this cannot be changed at runtime, we can cache it + static $funcExists = null; + if (null === $funcExists) { + $funcExists = function_exists('mb_strlen'); + } + if ($funcExists) { + return mb_strlen($string, '8bit'); + } + return strlen($string); + } + + /** + * Whether the current user is authenticated + * + * @return bool + */ + public function isAuthenticated() { + $storedHash = $this->updater->getConfigOption('updater.secret'); + + // As a sanity check the stored hash or the sent password can never be empty + if($storedHash === '' || $storedHash === null || $this->password === null) { + return false; + } + + // As we still support PHP 5.4 we have to use some magic involving "crypt" + return $this->equals($storedHash, crypt($this->password, $storedHash)); + } +} + class Updater { /** @var array */ private $configValues = []; @@ -116,7 +207,7 @@ public function updateAvailable() { * @param string $key * @return mixed|null Null if the entry is not found */ - private function getConfigOption($key) { + public function getConfigOption($key) { return isset($this->configValues[$key]) ? $this->configValues[$key] : null; } @@ -650,11 +741,17 @@ public function moveNewVersionInPlace() { die($e->getMessage()); } +// Check for authentication +$password = isset($_SERVER['HTTP_X_UPDATER_AUTH']) ? $_SERVER['HTTP_X_UPDATER_AUTH'] : ''; +$auth = new Auth($updater, $password); + // TODO: Note when a step started and when one ended, also to prevent multiple people at the same time accessing the updater if(isset($_POST['step'])) { set_time_limit(0); try { - + if(!$auth->isAuthenticated()) { + throw new Exception('Not authenticated'); + } switch ($_POST['step']) { case '1': $updater->checkForExpectedFilesAndFolders(); @@ -904,6 +1001,10 @@ public function moveNewVersionInPlace() { color: #111; } + code { + color: lightcoral; + } + @@ -912,7 +1013,8 @@ public function moveNewVersionInPlace() {
To login you need to provide the unhashed value of "updater.secret" in your config file.
+If you don't know that value, you can access this updater directly via the Nextcloud admin screen or generate + your own secret:
+php -r '$password = trim(shell_exec("openssl rand -base64 48"));if(strlen($password) === 64) {$hash = password_hash($password, PASSWORD_DEFAULT) . "\n"; echo "Insert as \"updater.secret\": ".$hash; echo "The plaintext value is: ".$password."\n";}else{echo "Could not execute OpenSSL.\n";};'
+ Invalid password
+ +Please provide your defined password in config.php to proceed:
- - -