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) {