Added bad login IP address limitation.

This commit is contained in:
Ylian Saint-Hilaire 2019-11-18 17:31:42 -08:00
parent dfbd933dc7
commit c9dc923db1
14 changed files with 84 additions and 17 deletions

16
db.js
View File

@ -677,7 +677,13 @@ module.exports.CreateDB = function (parent, func) {
// TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch. // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
// https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/ // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
//obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } } //obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } } obj.isMaxType = function (max, type, domainid, func) {
if (obj.eventsfile.countDocuments) {
if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
} else {
if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
}
}
// Database actions on the events collection // Database actions on the events collection
obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); }; obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
@ -693,7 +699,13 @@ module.exports.CreateDB = function (parent, func) {
obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); }; obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); }; obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); }; obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
obj.GetFailedLoginCount = function (username, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null)?count:0); }); } obj.GetFailedLoginCount = function (username, domainid, lastlogin, func) {
if (obj.eventsfile.countDocuments) {
obj.eventsfile.countDocuments({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
} else {
obj.eventsfile.count({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
}
}
// Database actions on the power collection // Database actions on the power collection
obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); }; obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };

View File

@ -223,7 +223,6 @@ module.exports.CreateLetsEncrypt = function (parent) {
var somethingIsinFolder = false; var somethingIsinFolder = false;
try { try {
var filesinFolder = require('fs').readdirSync(obj.runAsProduction ? obj.configPath : obj.configPathStaging); var filesinFolder = require('fs').readdirSync(obj.runAsProduction ? obj.configPath : obj.configPathStaging);
console.log('filesinFolder', filesinFolder);
somethingIsinFolder = (filesinFolder.indexOf(obj.runAsProduction ? 'live' : 'staging') != -1); somethingIsinFolder = (filesinFolder.indexOf(obj.runAsProduction ? 'live' : 'staging') != -1);
} catch (ex) { console.log(ex); } } catch (ex) { console.log(ex); }

View File

@ -689,14 +689,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
case 'help': { case 'help': {
r = 'Available commands: help, info, versions, args, resetserver, showconfig, usersessions, tasklimiter, setmaxtasks, cores,\r\n' r = 'Available commands: help, info, versions, args, resetserver, showconfig, usersessions, tasklimiter, setmaxtasks, cores,\r\n'
r += 'migrationagents, agentstats, webstats, mpsstats, swarmstats, acceleratorsstats, updatecheck, serverupdate, nodeconfig,\r\n'; r += 'migrationagents, agentstats, webstats, mpsstats, swarmstats, acceleratorsstats, updatecheck, serverupdate, nodeconfig,\r\n';
r += 'heapdump, relays, autobackup, backupconfig, dupagents, dispatchtable.'; r += 'heapdump, relays, autobackup, backupconfig, dupagents, dispatchtable, badlogins.';
break;
}
case 'badlogins': {
r = "Max is " + parent.parent.config.settings.maxinvalidlogin.count + " bad login(s) in " + parent.parent.config.settings.maxinvalidlogin.time + " minute(s).\r\n";
var badLoginCount = 0;
parent.cleanBadLoginTable();
for (var i in parent.badLoginTable) { badLoginCount++; r += (i + ' - ' + parent.badLoginTable[i].length + " entries\r\n"); }
if (badLoginCount == 0) { r += 'No bad logins.'; }
break; break;
} }
case 'dispatchtable': { case 'dispatchtable': {
r = ''; r = '';
for (var i in parent.parent.eventsDispatch) { for (var i in parent.parent.eventsDispatch) { r += (i + ', ' + parent.parent.eventsDispatch[i].length + '\r\n'); }
r += (i + ', ' + parent.parent.eventsDispatch[i].length + '\r\n');
}
break; break;
} }
case 'dupagents': { case 'dupagents': {

View File

@ -1,6 +1,6 @@
{ {
"name": "meshcentral", "name": "meshcentral",
"version": "0.4.4-l", "version": "0.4.4-m",
"keywords": [ "keywords": [
"Remote Management", "Remote Management",
"Intel AMT", "Intel AMT",

View File

@ -58,7 +58,9 @@
}, },
"_Redirects": { "_Redirects": {
"meshcommander": "https://www.meshcommander.com/" "meshcommander": "https://www.meshcommander.com/"
} },
"__MaxInvalidLogin": "Time in minutes, max amount of bad logins from a source IP in the time before logins are rejected.",
"MaxInvalidLogin": { "time": 10, "count": 10 }
}, },
"_domains": { "_domains": {
"": { "": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -270,7 +270,7 @@
// Display the right server message // Display the right server message
var messageid = parseInt('{{{messageid}}}'); var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."]; var okmessages = ['', "Hold on, reset mail sent."];
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."]; var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) { if (messageid > 0) {
var msg = ''; var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; } if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }

View File

@ -267,7 +267,7 @@
// Display the right server message // Display the right server message
var messageid = parseInt('{{{messageid}}}'); var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."]; var okmessages = ['', "Hold on, reset mail sent."];
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."]; var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) { if (messageid > 0) {
var msg = ''; var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; } if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -268,7 +268,7 @@
// Display the right server message // Display the right server message
var messageid = parseInt('{{{messageid}}}'); var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."]; var okmessages = ['', "Hold on, reset mail sent."];
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."]; var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) { if (messageid > 0) {
var msg = ''; var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; } if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }

View File

@ -265,7 +265,7 @@
// Display the right server message // Display the right server message
var messageid = parseInt('{{{messageid}}}'); var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."]; var okmessages = ['', "Hold on, reset mail sent."];
var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."]; var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) { if (messageid > 0) {
var msg = ''; var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; } if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }

View File

@ -639,6 +639,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
const domain = checkUserIpAddress(req, res); const domain = checkUserIpAddress(req, res);
if (domain == null) { parent.debug('web', 'handleLoginRequest: invalid domain'); res.sendStatus(404); return; } if (domain == null) { parent.debug('web', 'handleLoginRequest: invalid domain'); res.sendStatus(404); return; }
// Check if this is a banned ip address
if (obj.checkAllowLogin(req) == false) {
// Wait and redirect the user
setTimeout(function () {
req.session.messageid = 114; // IP address blocked, try again later.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
return;
}
// Normally, use the body username/password. If this is a token, use the username/password in the session. // Normally, use the body username/password. If this is a token, use the username/password in the session.
var xusername = req.body.username, xpassword = req.body.password; var xusername = req.body.username, xpassword = req.body.password;
if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; } if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; }
@ -660,6 +670,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
req.session.messageid = 108; // Invalid token, try again. req.session.messageid = 108; // Invalid token, try again.
parent.debug('web', 'handleLoginRequest: invalid 2FA token'); parent.debug('web', 'handleLoginRequest: invalid 2FA token');
obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + user.name], obj, { action: 'authfail', username: user.name, userid: 'user/' + domain.id + '/' + user.name, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + cleanRemoteAddr(req.ip) }); obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + user.name], obj, { action: 'authfail', username: user.name, userid: 'user/' + domain.id + '/' + user.name, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + cleanRemoteAddr(req.ip) });
obj.setbadLogin(req);
} else { } else {
parent.debug('web', 'handleLoginRequest: 2FA token required'); parent.debug('web', 'handleLoginRequest: 2FA token required');
} }
@ -693,10 +704,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
parent.debug('web', 'handleLoginRequest: login failed, locked account'); parent.debug('web', 'handleLoginRequest: login failed, locked account');
req.session.messageid = 110; // Account locked. req.session.messageid = 110; // Account locked.
obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'User login attempt on locked account from ' + cleanRemoteAddr(req.ip) }); obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'User login attempt on locked account from ' + cleanRemoteAddr(req.ip) });
obj.setbadLogin(req);
} else { } else {
parent.debug('web', 'handleLoginRequest: login failed, bad username and password'); parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
req.session.messageid = 112; // Login failed, check username and password. req.session.messageid = 112; // Login failed, check username and password.
obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + cleanRemoteAddr(req.ip) }); obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + cleanRemoteAddr(req.ip) });
obj.setbadLogin(req);
} }
} }
@ -1015,6 +1028,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if ((req.body.token != null) || (req.body.hwtoken != null)) { if ((req.body.token != null) || (req.body.hwtoken != null)) {
req.session.messageid = 108; // Invalid token, try again. req.session.messageid = 108; // Invalid token, try again.
obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + user.name], obj, { action: 'authfail', username: user.name, userid: 'user/' + domain.id + '/' + user.name, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + cleanRemoteAddr(req.ip) }); obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + user.name], obj, { action: 'authfail', username: user.name, userid: 'user/' + domain.id + '/' + user.name, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + cleanRemoteAddr(req.ip) });
obj.setbadLogin(req);
} }
req.session.loginmode = '5'; req.session.loginmode = '5';
req.session.tokenemail = email; req.session.tokenemail = email;
@ -3434,6 +3448,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Authenticates a session and forwards // Authenticates a session and forwards
function PerformWSSessionAuth(ws, req, noAuthOk, func) { function PerformWSSessionAuth(ws, req, noAuthOk, func) {
// Check if this is a banned ip address
if (obj.checkAllowLogin(req) == false) { try { ws.send(JSON.stringify({ action: 'close', cause: 'banned', msg: 'banned-1' })); ws.close(); } catch (e) { } return; }
try { try {
// Hold this websocket until we are ready. // Hold this websocket until we are ready.
ws._socket.pause(); ws._socket.pause();
@ -3476,6 +3492,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// If not authenticated, close the websocket connection // If not authenticated, close the websocket connection
parent.debug('web', 'ERR: Websocket bad user/pass auth'); parent.debug('web', 'ERR: Websocket bad user/pass auth');
//obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + obj.args.user.toLowerCase()], obj, { action: 'authfail', userid: 'user/' + domain.id + '/' + obj.args.user.toLowerCase(), username: obj.args.user, domain: domain.id, msg: 'Invalid user login attempt from ' + cleanRemoteAddr(req.ip) }); //obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + obj.args.user.toLowerCase()], obj, { action: 'authfail', userid: 'user/' + domain.id + '/' + obj.args.user.toLowerCase(), username: obj.args.user, domain: domain.id, msg: 'Invalid user login attempt from ' + cleanRemoteAddr(req.ip) });
//obj.setbadLogin(req);
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2' })); ws.close(); } catch (e) { } try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2' })); ws.close(); } catch (e) { }
} }
} }
@ -4014,5 +4031,36 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
} catch (ex) { console.log(ex); func(fd, tag); } } catch (ex) { console.log(ex); func(fd, tag); }
} }
// This is the invalid login throttling code
obj.badLoginTable = {};
obj.badLoginTableLastClean = 0;
if (parent.config.settings == null) { parent.config.settings = {}; }
if (parent.config.settings.maxinvalidlogin == null) { parent.config.settings.maxinvalidlogin = { time: 10, count: 10 }; }
if (typeof parent.config.settings.maxinvalidlogin.time != 'number') { parent.config.settings.maxinvalidlogin.time = 10; }
if (typeof parent.config.settings.maxinvalidlogin.count != 'number') { parent.config.settings.maxinvalidlogin.count = 10; }
obj.setbadLogin = function (ip) { // Set an IP address that just did a bad login request
if (typeof ip == 'object') { ip = cleanRemoteAddr(ip.ip); }
if (++obj.badLoginTableLastClean > 100) { obj.cleanBadLoginTable(); }
if (obj.badLoginTable[ip] == null) { obj.badLoginTable[ip] = [Date.now()]; } else { obj.badLoginTable[ip].push(Date.now()); }
}
obj.checkAllowLogin = function (ip) { // Check if an IP address is allowed to login
if (typeof ip == 'object') { ip = cleanRemoteAddr(ip.ip); }
var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
var ipTable = obj.badLoginTable[ip];
if (ipTable == null) return true;
while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
if (ipTable.length == 0) { delete obj.badLoginTable[ip]; return true; }
return (ipTable.length < parent.config.settings.maxinvalidlogin.count); // No more than x bad logins in x minutes
}
obj.cleanBadLoginTable = function () { // Clean up the IP address login blockage table, we do this occasionaly.
var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
for (var i in ipTable) {
var ipTable = obj.badLoginTable[ip];
while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
if (ipTable.length == 0) { delete obj.badLoginTable[ip]; }
}
obj.badLoginTableLastClean = 0;
}
return obj; return obj;
}; };