]> git.joonet.de Git - adminer.git/commitdiff
Prevent against brute force login attempts from the same IP address
authorJakub Vrana <jakub@vrana.cz>
Sat, 22 Mar 2014 05:47:34 +0000 (22:47 -0700)
committerJakub Vrana <jakub@vrana.cz>
Sat, 22 Mar 2014 05:47:57 +0000 (22:47 -0700)
adminer/include/adminer.inc.php
adminer/include/auth.inc.php
adminer/include/functions.inc.php
adminer/include/pdo.inc.php
adminer/lang/cs.inc.php
adminer/lang/en.inc.php
adminer/lang/xx.inc.php
changes.txt
editor/include/adminer.inc.php

index 3553925dc021583a353bec6aa905fce70a00e44b..cbd526bd1b8609dda35e82fc69b64fc9e0d71de0 100644 (file)
@@ -27,6 +27,13 @@ class Adminer {
                return password_file($create);
        }
 
+       /** Return key used to group brute force attacks; behind a reverse proxy, you want to return the last part of X-Forwarded-For
+       * @return string
+       */
+       function bruteForceKey() {
+               return $_SERVER["REMOTE_ADDR"];
+       }
+
        /** Identifier of selected database
        * @return string
        */
index fc30d2c0b198dee426c5fca8185ca38c24f63088..b64e5ba434f81053be55714dddc2f8be04b5326d 100644 (file)
@@ -15,8 +15,47 @@ if ($_COOKIE["adminer_permanent"]) {
        }
 }
 
+function add_invalid_login() {
+       global $adminer;
+       $filename = get_temp_dir() . "/adminer.invalid";
+       $fp = @fopen($filename, "r+"); // @ - may not exist
+       if (!$fp) { // c+ is available since PHP 5.2.6
+               $fp = fopen($filename, "w");
+               if (!$fp) {
+                       return;
+               }
+       }
+       flock($fp, LOCK_EX);
+       $invalids = unserialize(stream_get_contents($fp));
+       $time = time();
+       if ($invalids) {
+               foreach ($invalids as $ip => $val) {
+                       if ($val[0] < $time) {
+                               unset($invalids[$ip]);
+                       }
+               }
+       }
+       $invalid = &$invalids[$adminer->bruteForceKey()];
+       if (!$invalid) {
+               $invalid = array($time + 30*60, 0); // active for 30 minutes
+       }
+       $invalid[1]++;
+       $serialized = serialize($invalids);
+       rewind($fp);
+       fwrite($fp, $serialized);
+       ftruncate($fp, strlen($serialized));
+       flock($fp, LOCK_UN);
+       fclose($fp);
+}
+
 $auth = $_POST["auth"];
 if ($auth) {
+       $invalids = unserialize(@file_get_contents(get_temp_dir() . "/adminer.invalid")); // @ - may not exist
+       $invalid = $invalids[$adminer->bruteForceKey()];
+       $next_attempt = ($invalid[1] > 30 ? $invalid[0] - time() : 0); // allow 30 invalid attempts
+       if ($next_attempt > 0) { //! do the same with permanent login
+               auth_error(lang('Too many unsuccessful logins, try again in %d minute(s).', ceil($next_attempt / 60)));
+       }
        session_regenerate_id(); // defense against session fixation
        $driver = $auth["driver"];
        $server = $auth["server"];
@@ -75,19 +114,18 @@ function unset_permanent() {
        cookie("adminer_permanent", implode(" ", $permanent));
 }
 
-function auth_error($exception = null) {
-       global $connection, $adminer, $has_token;
+function auth_error($error) {
+       global $adminer, $has_token;
        $session_name = session_name();
-       $error = "";
        if (!$_COOKIE[$session_name] && $_GET[$session_name] && ini_bool("session.use_only_cookies")) {
                $error = lang('Session support must be enabled.');
        } elseif (isset($_GET["username"])) {
                if (($_COOKIE[$session_name] || $_GET[$session_name]) && !$has_token) {
                        $error = lang('Session expired, please login again.');
                } else {
+                       add_invalid_login();
                        $password = get_password();
                        if ($password !== null) {
-                               $error = h($exception ? $exception->getMessage() : (is_string($connection) ? $connection : lang('Invalid credentials.')));
                                if ($password === false) {
                                        $error .= '<br>' . lang('Master password expired. <a href="http://www.adminer.org/en/extension/" target="_blank">Implement</a> %s method to make it permanent.', '<code>permanentLogin()</code>');
                                }
@@ -106,6 +144,7 @@ function auth_error($exception = null) {
        echo "</div>\n";
        echo "</form>\n";
        page_footer("auth");
+       exit;
 }
 
 if (isset($_GET["username"])) {
@@ -122,8 +161,7 @@ if (isset($_GET["username"])) {
 $driver = new Min_Driver($connection);
 
 if (!is_object($connection) || !$adminer->login($_GET["username"], get_password())) {
-       auth_error();
-       exit;
+       auth_error((is_string($connection) ? $connection : lang('Invalid credentials.')));
 }
 
 if ($auth && $_POST["token"]) {
index 9ecd43b691eb4f125bee32bdfa05eb4cc828210e..d328f65f724cae3f990f7e0d4d313d06e7307a80 100644 (file)
@@ -1034,26 +1034,33 @@ function apply_sql_function($function, $column) {
        return ($function ? ($function == "unixepoch" ? "DATETIME($column, '$function')" : ($function == "count distinct" ? "COUNT(DISTINCT " : strtoupper("$function(")) . "$column)") : $column);
 }
 
-/** Read password from file adminer.key in temporary directory or create one
-* @param bool
-* @return string or false if the file can not be created
+/** Get path of the temporary directory
+* @return string
 */
-function password_file($create) {
-       $dir = ini_get("upload_tmp_dir"); // session_save_path() may contain other storage path
-       if (!$dir) {
+function get_temp_dir() {
+       $return = ini_get("upload_tmp_dir"); // session_save_path() may contain other storage path
+       if (!$return) {
                if (function_exists('sys_get_temp_dir')) {
-                       $dir = sys_get_temp_dir();
+                       $return = sys_get_temp_dir();
                } else {
                        $filename = @tempnam("", ""); // @ - temp directory can be disabled by open_basedir
                        if (!$filename) {
                                return false;
                        }
-                       $dir = dirname($filename);
+                       $return = dirname($filename);
                        unlink($filename);
                }
        }
-       $filename = "$dir/adminer.key";
-       $return = @file_get_contents($filename); // @ - can not exist
+       return $return;
+}
+
+/** Read password from file adminer.key in temporary directory or create one
+* @param bool
+* @return string or false if the file can not be created
+*/
+function password_file($create) {
+       $filename = get_temp_dir() . "/adminer.key";
+       $return = @file_get_contents($filename); // @ - may not exist
        if ($return || !$create) {
                return $return;
        }
index a47953424ff2d9983c76b06fed1177eada0e887a..b87eed6d9d87dab48abac012ba98579203c4332f 100644 (file)
@@ -16,8 +16,7 @@ if (extension_loaded('pdo')) {
                        try {
                                parent::__construct($dsn, $username, $password);
                        } catch (Exception $ex) {
-                               auth_error($ex);
-                               exit;
+                               auth_error($ex->getMessage());
                        }
                        $this->setAttribute(13, array('Min_PDOStatement')); // 13 - PDO::ATTR_STATEMENT_CLASS
                        $this->server_info = $this->getAttribute(4); // 4 - PDO::ATTR_SERVER_VERSION
index 9e0cf0678a6d05af23cf1825a8cdb18518d811a7..0ff4d538a1efb5e7920874b6422c6e80c6f8ffc4 100644 (file)
@@ -11,6 +11,7 @@ $translations = array(
        'Logged as: %s' => 'Přihlášen jako: %s',
        'Logout successful.' => 'Odhlášení proběhlo v pořádku.',
        'Invalid credentials.' => 'Neplatné přihlašovací údaje.',
+       'Too many unsuccessful logins, try again in %d minute(s).' => array('Příliš mnoho pokusů o přihlášení, zkuste to znovu za %d minutu.', 'Příliš mnoho pokusů o přihlášení, zkuste to znovu za %d minuty.', 'Příliš mnoho pokusů o přihlášení, zkuste to znovu za %d minut.'),
        'Master password expired. <a href="http://www.adminer.org/en/extension/" target="_blank">Implement</a> %s method to make it permanent.' => 'Platnost hlavního hesla vypršela. <a href="http://www.adminer.org/cs/extension/" target="_blank">Implementujte</a> metodu %s, aby platilo stále.',
        'Language' => 'Jazyk',
        'Invalid CSRF token. Send the form again.' => 'Neplatný token CSRF. Odešlete formulář znovu.',
index 1c2bdd758eae6808ed1e53881d1cdb9a8bb348c5..52559f98019160e8c7c5c452c5c594adf3ed39e4 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 $translations = array(
+       'Too many unsuccessful logins, try again in %d minute(s).' => array('Too many unsuccessful logins, try again in %d minute.', 'Too many unsuccessful logins, try again in %d minutes.'),
        'Query executed OK, %d row(s) affected.' => array('Query executed OK, %d row affected.', 'Query executed OK, %d rows affected.'),
        '%d byte(s)' => array('%d byte', '%d bytes'),
        'Routine has been called, %d row(s) affected.' => array('Routine has been called, %d row affected.', 'Routine has been called, %d rows affected.'),
index e031726ef84614f99c33e0f321e05720f8a0b938..cbfa94cfde973af6fcbd53bf107a8ddb5aae40f8 100644 (file)
@@ -11,6 +11,7 @@ $translations = array(
        'Logged as: %s' => 'xx',
        'Logout successful.' => 'xx',
        'Invalid credentials.' => 'xx',
+       'Too many unsuccessful logins, try again in %d minute(s).' => array('xx', 'xx'),
        'Master password expired. <a href="http://www.adminer.org/en/extension/" target="_blank">Implement</a> %s method to make it permanent.' => 'xx',
        'Language' => 'xx',
        'Invalid CSRF token. Send the form again.' => 'xx',
index ff401550eb29f761b491e3618b3dd5c793a73e5d..43e9b1349603e49f61f43dff3e604e7b79ee2577 100644 (file)
@@ -1,5 +1,6 @@
 Adminer 4.1.0-dev:
 Provide size of all databases in the overview
+Prevent against brute force login attempts from the same IP address
 Compute number of tables in the overview explicitly
 Display edit form after error in clone or multi-edit
 Trim trailing non-breaking spaces in SQL textarea
index 9635aa8aa097915b468433c93731e87089293a1e..a0e0a09e0d864648e7c2271eec6c2163d7f5bb9f 100644 (file)
@@ -17,6 +17,10 @@ class Adminer {
                return password_file($create);
        }
 
+       function bruteForceKey() {
+               return $_SERVER["REMOTE_ADDR"];
+       }
+
        function database() {
                global $connection;
                if ($connection) {