Commit 83c81d89 authored by Vladislav Vaintroub's avatar Vladislav Vaintroub

MDEV-7598 Lock user after too many password errors

parent 30da40bb
--source include/not_embedded.inc
if (!$AUTH_NAMED_PIPE_SO) {
skip No auth_named_pipe plugin;
}
if (!$USERNAME) {
skip USER variable is undefined;
}
if (`SELECT count(*) <> 0 FROM mysql.user WHERE user = '$USERNAME'`) {
skip %USERNAME%=$USER which exists in mysql.user;
}
set @old_max_password_errors=@@max_password_errors;
set global max_password_errors=2;
create user u identified by 'good_pass';
connect(localhost,u,bas_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, u, bas_pass;
ERROR 28000: Access denied for user 'u'@'localhost' (using password: YES)
connect(localhost,u,bad_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, u, bad_pass;
ERROR 28000: Access denied for user 'u'@'localhost' (using password: YES)
connect(localhost,u,good_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, u, good_pass;
ERROR HY000: User is blocked because of too many credential errors; unblock with 'FLUSH PRIVILEGES'
connect(localhost,u,bad_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, u, bad_pass;
ERROR HY000: User is blocked because of too many credential errors; unblock with 'FLUSH PRIVILEGES'
FLUSH PRIVILEGES;
connect con1, localhost, u, good_pass;
disconnect con1;
connect(localhost,u,bad_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, u, bad_pass;
ERROR 28000: Access denied for user 'u'@'localhost' (using password: YES)
connect con1, localhost, u, good_pass;
disconnect con1;
connect(localhost,u,bad_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, u, bad_pass;
ERROR 28000: Access denied for user 'u'@'localhost' (using password: YES)
connect con1, localhost, u, good_pass;
ERROR 28000: Access denied for user 'u'@'localhost' (using password: YES)
ERROR 28000: Access denied for user 'u'@'localhost' (using password: YES)
ERROR HY000: User is blocked because of too many credential errors; unblock with 'FLUSH PRIVILEGES'
disconnect con1;
connection default;
FLUSH PRIVILEGES;
connect(localhost,root,bas_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, root, bas_pass;
ERROR 28000: Access denied for user 'root'@'localhost' (using password: YES)
connect(localhost,root,bad_pass,test,MASTER_PORT,MASTER_SOCKET);
connect con1, localhost, root, bad_pass;
ERROR 28000: Access denied for user 'root'@'localhost' (using password: YES)
connect con1, localhost, u, good_pass;
disconnect con1;
connection default;
DROP USER u;
FLUSH PRIVILEGES;
set global max_password_errors=@old_max_password_errors;
--source include/not_embedded.inc
set @old_max_password_errors=@@max_password_errors;
set global max_password_errors=2;
create user u identified by 'good_pass';
# Test that user is blocked after 'max_password_errors' bad passwords
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
connect(con1, localhost, u, bas_pass);
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
connect (con1, localhost, u, bad_pass);
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_USER_IS_BLOCKED;
connect(con1, localhost, u, good_pass);
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_USER_IS_BLOCKED;
connect(con1, localhost, u, bad_pass);
# Test that FLUSH PRIVILEGES clears the error
FLUSH PRIVILEGES;
connect (con1, localhost, u, good_pass);
disconnect con1;
# Test that good login clears the error
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
connect (con1, localhost, u, bad_pass);
connect (con1, localhost, u, good_pass);
disconnect con1;
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
connect (con1, localhost, u, bad_pass);
connect (con1, localhost, u, good_pass);
# Test the behavior of change_user
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
change_user u,bad_pass;
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
change_user u,bad_pass;
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_USER_IS_BLOCKED;
change_user u,good_pass;
disconnect con1;
connection default;
FLUSH PRIVILEGES;
#Test that root@localhost is not blocked, with password errors
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
connect(con1, localhost, root, bas_pass);
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_ERROR;
connect (con1, localhost, root, bad_pass);
connect (con1, localhost, u, good_pass);
disconnect con1;
connection default;
DROP USER u;
FLUSH PRIVILEGES;
set global max_password_errors=@old_max_password_errors;
\ No newline at end of file
......@@ -546,6 +546,10 @@ The following specify which files/extra groups are read (specified before remain
The maximum BLOB length to send to server from
mysql_send_long_data API. Deprecated option; use
max_allowed_packet instead.
--max-password-errors=#
If there is more than this number of failed connect
attempts due to invalid password, user will be blocked
from further connections until FLUSH_PRIVILEGES.
--max-prepared-stmt-count=#
Maximum number of prepared statements in the server
--max-recursive-iterations[=#]
......@@ -1518,6 +1522,7 @@ max-heap-table-size 16777216
max-join-size 18446744073709551615
max-length-for-sort-data 1024
max-long-data-size 16777216
max-password-errors 18446744073709551615
max-prepared-stmt-count 16382
max-recursive-iterations 18446744073709551615
max-relay-log-size 1073741824
......
set @old_max_password_errors=@@max_password_errors;
create user nosuchuser identified with 'named_pipe';
set global max_password_errors=1;
connect(localhost,nosuchuser,,test,MASTER_PORT,MASTER_SOCKET);
connect pipe_con,localhost,nosuchuser,,,,,PIPE;
ERROR 28000: Access denied for user 'nosuchuser'@'localhost'
connect(localhost,nosuchuser,,test,MASTER_PORT,MASTER_SOCKET);
connect pipe_con,localhost,nosuchuser,,,,,PIPE;
ERROR 28000: Access denied for user 'nosuchuser'@'localhost'
DROP USER nosuchuser;
FLUSH PRIVILEGES;
set global max_password_errors=@old_max_password_errors;
set @old_max_password_errors=@@max_password_errors;
create user nosuchuser identified with 'unix_socket';
set global max_password_errors=1;
connect(localhost,nosuchuser,,test,MASTER_PORT,MASTER_SOCKET);
connect pipe_con,localhost,nosuchuser;
ERROR 28000: Access denied for user 'nosuchuser'@'localhost'
connect(localhost,nosuchuser,,test,MASTER_PORT,MASTER_SOCKET);
connect pipe_con,localhost,nosuchuser;
ERROR 28000: Access denied for user 'nosuchuser'@'localhost'
DROP USER nosuchuser;
FLUSH PRIVILEGES;
set global max_password_errors=@old_max_password_errors;
--loose-enable-named-pipe --plugin-load=$AUTH_NAMED_PIPE_SO
# Tests that max_password_errors has no effect on login errors with
# passwordless plugins (Windows version / auth_named_pipe)
--source include/not_embedded.inc
--source include/have_auth_named_pipe.inc
if (`SELECT '$USERNAME' = 'nosuchuser'`) {
skip skipped for nosuchuser;
}
set @old_max_password_errors=@@max_password_errors;
create user nosuchuser identified with 'named_pipe';
set global max_password_errors=1;
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_NO_PASSWORD_ERROR;
connect(pipe_con,localhost,nosuchuser,,,,,PIPE);
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_NO_PASSWORD_ERROR;
connect(pipe_con,localhost,nosuchuser,,,,,PIPE);
DROP USER nosuchuser;
FLUSH PRIVILEGES;
set global max_password_errors=@old_max_password_errors;
--loose-enable-named-pipe --plugin-load=$AUTH_SOCKET_SO
# Tests that max_password_errors has no effect on login errors with
# passwordless plugins (Unix version / auth_unix_socket)
--source include/not_embedded.inc
--source include/have_unix_socket.inc
if (`SELECT '$USER' = 'nosuchuser'`) {
skip USER is nosuchuser;
}
set @old_max_password_errors=@@max_password_errors;
create user nosuchuser identified with 'unix_socket';
set global max_password_errors=1;
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_NO_PASSWORD_ERROR;
connect(pipe_con,localhost,nosuchuser);
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT
error ER_ACCESS_DENIED_NO_PASSWORD_ERROR;
connect(pipe_con,localhost,nosuchuser);
DROP USER nosuchuser;
FLUSH PRIVILEGES;
set global max_password_errors=@old_max_password_errors;
......@@ -2210,6 +2210,20 @@ NUMERIC_BLOCK_SIZE 1
ENUM_VALUE_LIST NULL
READ_ONLY YES
COMMAND_LINE_ARGUMENT REQUIRED
VARIABLE_NAME MAX_PASSWORD_ERRORS
SESSION_VALUE NULL
GLOBAL_VALUE 4294967295
GLOBAL_VALUE_ORIGIN COMPILE-TIME
DEFAULT_VALUE 4294967295
VARIABLE_SCOPE GLOBAL
VARIABLE_TYPE INT UNSIGNED
VARIABLE_COMMENT If there is more than this number of failed connect attempts due to invalid password, user will be blocked from further connections until FLUSH_PRIVILEGES.
NUMERIC_MIN_VALUE 1
NUMERIC_MAX_VALUE 4294967295
NUMERIC_BLOCK_SIZE 1
ENUM_VALUE_LIST NULL
READ_ONLY NO
COMMAND_LINE_ARGUMENT REQUIRED
VARIABLE_NAME MAX_PREPARED_STMT_COUNT
SESSION_VALUE NULL
GLOBAL_VALUE 16382
......
......@@ -2420,6 +2420,20 @@ NUMERIC_BLOCK_SIZE 1
ENUM_VALUE_LIST NULL
READ_ONLY YES
COMMAND_LINE_ARGUMENT REQUIRED
VARIABLE_NAME MAX_PASSWORD_ERRORS
SESSION_VALUE NULL
GLOBAL_VALUE 4294967295
GLOBAL_VALUE_ORIGIN COMPILE-TIME
DEFAULT_VALUE 4294967295
VARIABLE_SCOPE GLOBAL
VARIABLE_TYPE INT UNSIGNED
VARIABLE_COMMENT If there is more than this number of failed connect attempts due to invalid password, user will be blocked from further connections until FLUSH_PRIVILEGES.
NUMERIC_MIN_VALUE 1
NUMERIC_MAX_VALUE 4294967295
NUMERIC_BLOCK_SIZE 1
ENUM_VALUE_LIST NULL
READ_ONLY NO
COMMAND_LINE_ARGUMENT REQUIRED
VARIABLE_NAME MAX_PREPARED_STMT_COUNT
SESSION_VALUE NULL
GLOBAL_VALUE 16382
......
......@@ -513,6 +513,7 @@ ulong specialflag=0;
ulong binlog_cache_use= 0, binlog_cache_disk_use= 0;
ulong binlog_stmt_cache_use= 0, binlog_stmt_cache_disk_use= 0;
ulong max_connections, max_connect_errors;
uint max_password_errors;
ulong extra_max_connections;
uint max_digest_length= 0;
ulong slave_retried_transactions;
......
......@@ -241,6 +241,7 @@ extern ulong slow_launch_threads, slow_launch_time;
extern MYSQL_PLUGIN_IMPORT ulong max_connections;
extern uint max_digest_length;
extern ulong max_connect_errors, connect_timeout;
extern uint max_password_errors;
extern my_bool slave_allow_batching;
extern my_bool allow_slave_start;
extern LEX_CSTRING reason_slave_blocked;
......
......@@ -7931,3 +7931,5 @@ ER_BACKUP_STAGE_FAILED
eng "Backup stage '%s' failed"
ER_BACKUP_UNKNOWN_STAGE
eng "Unknown backup stage: '%s'. Stage should be one of START, FLUSH, BLOCK_DDL, BLOCK_COMMIT or END"
ER_USER_IS_BLOCKED
eng "User is blocked because of too many credential errors; unblock with 'FLUSH PRIVILEGES'"
......@@ -147,6 +147,7 @@ class ACL_USER :public ACL_USER_BASE
size_t hostname_length;
USER_RESOURCES user_resource;
enum SSL_type ssl_type;
uint password_errors;
const char *ssl_cipher, *x509_issuer, *x509_subject;
LEX_CSTRING plugin;
LEX_CSTRING auth_string;
......@@ -12325,6 +12326,23 @@ static bool send_plugin_request_packet(MPVIO_EXT *mpvio,
}
#ifndef NO_EMBEDDED_ACCESS_CHECKS
/**
Safeguard to avoid blocking the root, when max_password_errors
limit is reached.
Currently, we allow password errors for superuser on localhost.
@return true, if password errors should be ignored, and user should not be locked.
*/
static bool ignore_max_password_errors(const ACL_USER *acl_user)
{
const char *host= acl_user->host.hostname;
return (acl_user->access & SUPER_ACL)
&& (!strcasecmp(host, "localhost") ||
!strcmp(host, "127.0.0.1") ||
!strcmp(host, "::1"));
}
/**
Finds acl entry in user database for authentication purposes.
......@@ -12343,6 +12361,16 @@ static bool find_mpvio_user(MPVIO_EXT *mpvio)
mysql_mutex_lock(&acl_cache->lock);
ACL_USER *user= find_user_or_anon(sctx->host, sctx->user, sctx->ip);
if (user && user->password_errors >= max_password_errors && !ignore_max_password_errors(user))
{
mysql_mutex_unlock(&acl_cache->lock);
my_error(ER_USER_IS_BLOCKED, MYF(0));
general_log_print(mpvio->auth_info.thd, COM_CONNECT,
ER_THD(mpvio->auth_info.thd, ER_USER_IS_BLOCKED));
DBUG_RETURN(1);
}
if (user)
mpvio->acl_user= user->copy(mpvio->auth_info.thd->mem_root);
......@@ -13188,6 +13216,38 @@ static int do_auth_once(THD *thd, const LEX_CSTRING *auth_plugin_name,
return res;
}
enum PASSWD_ERROR_ACTION
{
PASSWD_ERROR_CLEAR,
PASSWD_ERROR_INCREMENT
};
/* Increment, or clear password errors for a user. */
static void handle_password_errors(const char *user, const char *hostname, PASSWD_ERROR_ACTION action)
{
#ifndef NO_EMBEDDED_ACCESS_CHECKS
mysql_mutex_assert_not_owner(&acl_cache->lock);
mysql_mutex_lock(&acl_cache->lock);
ACL_USER *u = find_user_exact(hostname, user);
if (u)
{
switch(action)
{
case PASSWD_ERROR_INCREMENT:
u->password_errors++;
break;
case PASSWD_ERROR_CLEAR:
u->password_errors= 0;
break;
default:
DBUG_ASSERT(0);
break;
}
}
mysql_mutex_unlock(&acl_cache->lock);
#endif
}
/**
Perform the handshake, authorize the client and update thd sctx variables.
......@@ -13307,6 +13367,8 @@ bool acl_authenticate(THD *thd, uint com_change_user_pkt_len)
break;
case CR_AUTH_USER_CREDENTIALS:
errors.m_authentication= 1;
if (thd->password && !mpvio.make_it_fail)
handle_password_errors(acl_user->user.str, acl_user->host.hostname, PASSWD_ERROR_INCREMENT);
break;
case CR_ERROR:
default:
......@@ -13321,6 +13383,11 @@ bool acl_authenticate(THD *thd, uint com_change_user_pkt_len)
}
sctx->proxy_user[0]= 0;
if (thd->password && acl_user->password_errors)
{
/* Login succeeded, clear password errors.*/
handle_password_errors(acl_user->user.str, acl_user->host.hostname, PASSWD_ERROR_CLEAR);
}
if (initialized) // if not --skip-grant-tables
{
......
......@@ -1515,6 +1515,14 @@ static Sys_var_ulong Sys_max_connect_errors(
VALID_RANGE(1, UINT_MAX), DEFAULT(MAX_CONNECT_ERRORS),
BLOCK_SIZE(1));
static Sys_var_uint Sys_max_password_errors(
"max_password_errors",
"If there is more than this number of failed connect attempts "
"due to invalid password, user will be blocked from further connections until FLUSH_PRIVILEGES.",
GLOBAL_VAR(max_password_errors), CMD_LINE(REQUIRED_ARG),
VALID_RANGE(1, UINT_MAX), DEFAULT(UINT_MAX),
BLOCK_SIZE(1));
static Sys_var_uint Sys_max_digest_length(
"max_digest_length", "Maximum length considered for digest text.",
READ_ONLY GLOBAL_VAR(max_digest_length),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment