From 06f4346cfeec0e9f67a375708f9265557a738141 Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Fri, 21 Mar 2014 22:47:34 -0700 Subject: [PATCH] Prevent against brute force login attempts from the same IP address --- adminer/include/adminer.inc.php | 7 +++++ adminer/include/auth.inc.php | 50 +++++++++++++++++++++++++++---- adminer/include/functions.inc.php | 27 ++++++++++------- adminer/include/pdo.inc.php | 3 +- adminer/lang/cs.inc.php | 1 + adminer/lang/en.inc.php | 1 + adminer/lang/xx.inc.php | 1 + changes.txt | 1 + editor/include/adminer.inc.php | 4 +++ 9 files changed, 77 insertions(+), 18 deletions(-) diff --git a/adminer/include/adminer.inc.php b/adminer/include/adminer.inc.php index 3553925d..cbd526bd 100644 --- a/adminer/include/adminer.inc.php +++ b/adminer/include/adminer.inc.php @@ -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 */ diff --git a/adminer/include/auth.inc.php b/adminer/include/auth.inc.php index fc30d2c0..b64e5ba4 100644 --- a/adminer/include/auth.inc.php +++ b/adminer/include/auth.inc.php @@ -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 .= '
' . lang('Master password expired. Implement %s method to make it permanent.', 'permanentLogin()'); } @@ -106,6 +144,7 @@ function auth_error($exception = null) { echo "\n"; echo "\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"]) { diff --git a/adminer/include/functions.inc.php b/adminer/include/functions.inc.php index 9ecd43b6..d328f65f 100644 --- a/adminer/include/functions.inc.php +++ b/adminer/include/functions.inc.php @@ -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; } diff --git a/adminer/include/pdo.inc.php b/adminer/include/pdo.inc.php index a4795342..b87eed6d 100644 --- a/adminer/include/pdo.inc.php +++ b/adminer/include/pdo.inc.php @@ -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 diff --git a/adminer/lang/cs.inc.php b/adminer/lang/cs.inc.php index 9e0cf067..0ff4d538 100644 --- a/adminer/lang/cs.inc.php +++ b/adminer/lang/cs.inc.php @@ -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. Implement %s method to make it permanent.' => 'Platnost hlavního hesla vypršela. Implementujte metodu %s, aby platilo stále.', 'Language' => 'Jazyk', 'Invalid CSRF token. Send the form again.' => 'Neplatný token CSRF. Odešlete formulář znovu.', diff --git a/adminer/lang/en.inc.php b/adminer/lang/en.inc.php index 1c2bdd75..52559f98 100644 --- a/adminer/lang/en.inc.php +++ b/adminer/lang/en.inc.php @@ -1,5 +1,6 @@ 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.'), diff --git a/adminer/lang/xx.inc.php b/adminer/lang/xx.inc.php index e031726e..cbfa94cf 100644 --- a/adminer/lang/xx.inc.php +++ b/adminer/lang/xx.inc.php @@ -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. Implement %s method to make it permanent.' => 'xx', 'Language' => 'xx', 'Invalid CSRF token. Send the form again.' => 'xx', diff --git a/changes.txt b/changes.txt index ff401550..43e9b134 100644 --- a/changes.txt +++ b/changes.txt @@ -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 diff --git a/editor/include/adminer.inc.php b/editor/include/adminer.inc.php index 9635aa8a..a0e0a09e 100644 --- a/editor/include/adminer.inc.php +++ b/editor/include/adminer.inc.php @@ -17,6 +17,10 @@ class Adminer { return password_file($create); } + function bruteForceKey() { + return $_SERVER["REMOTE_ADDR"]; + } + function database() { global $connection; if ($connection) {