diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index c6fa923b..4522b820 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -160,7 +160,8 @@ "reset": { "type": "integer", "description": "Number of days after which the user is required to change the account password." }, "force2factor": { "type": "boolean", "description": "Requires that all accounts setup 2FA." }, "skip2factor": { "type": "string", "description": "IP addresses where 2FA login is skipped, for example: 127.0.0.1,192.168.2.0/24" }, - "oldPasswordBan": { "type": "integer", "description": "Number of old passwords the server should remember and not allow the user to switch back to." } + "oldPasswordBan": { "type": "integer", "description": "Number of old passwords the server should remember and not allow the user to switch back to." }, + "banCommonPasswords": { "type": "boolean", "description": "Uses WildLeek to block use of the 10000 most commonly used passwords." } } }, "agentInviteCodes": { "type": "boolean", "default": false, "description": "Enabled a feature where you can set one or more invitation codes in a device group. You can then give a invitation link to users who can use it to download the agent." }, diff --git a/meshcentral.js b/meshcentral.js index cbe914a8..fc613486 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -2646,6 +2646,9 @@ function mainStart() { // Lowercase the auth value if present for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } } + // Get the current node version + var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); + // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used var sspi = false; var ldap = false; @@ -2655,6 +2658,7 @@ function mainStart() { var mstsc = false; var recordingIndex = false; var domainCount = 0; + var wildleek = false; if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; } if (domainCount == 0) { allsspi = false; } for (var i in config.domains) { @@ -2672,11 +2676,9 @@ function mainStart() { if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); } } if ((config.domains[i].sessionrecording != null) && (config.domains[i].sessionrecording.index == true)) { recordingIndex = true; } + if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { if (nodeVersion < 8) { config.domains[i].passwordrequirements = false; addServerWarning('Common password checking requires NodeJS v8 or above.'); } else { wildleek = true; } } } - // Get the current node version - var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); - // Build the list of required modules var modules = ['ws', 'cbor', 'nedb', 'https', 'yauzl', 'xmldom', 'ipcheck', 'express', 'archiver@4.0.2', 'multiparty', 'node-forge', 'express-ws', 'compression', 'body-parser', 'connect-redis', 'cookie-session', 'express-handlebars']; if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); if (sspi == true) { modules.push('node-sspi'); } } // Add Windows modules @@ -2702,6 +2704,9 @@ function mainStart() { // Setup encrypted zip support if needed if (config.settings.autobackup && config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted'); } + // Setup common password blocking + if (wildleek == true) { modules.push('wildleek@2.0.0'); } + // Setup 2nd factor authentication if (config.settings.no2factorauth !== true) { // Setup YubiKey OTP if configured diff --git a/meshuser.js b/meshuser.js index eed4e221..51d21ee5 100644 --- a/meshuser.js +++ b/meshuser.js @@ -2375,9 +2375,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.checkUserPassword(domain, user, command.oldpass, function (result) { if (result == true) { parent.checkOldUserPasswords(domain, user, command.newpass, function (result) { - if (result == true) { + if (result == 1) { // Send user notification of error displayNotificationMessage('Error, unable to change to previously used password.', 'Account Settings', 'ServerNotify'); + } else if (result == 2) { + // Send user notification of error + displayNotificationMessage('Error, unable to change to commonly used password.', 'Account Settings', 'ServerNotify'); } else { // Update the password require('./pass').hash(command.newpass, function (err, salt, hash, tag) { diff --git a/sample-config-advanced.json b/sample-config-advanced.json index 62c248e8..b0a27459 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -136,7 +136,9 @@ "nonalpha": 1, "reset": 90, "force2factor": true, - "skip2factor": "127.0.0.1,192.168.2.0/24" + "skip2factor": "127.0.0.1,192.168.2.0/24", + "oldPasswordBan": 5, + "banCommonPasswords": false }, "_agentInviteCodes": true, "_agentNoProxy": true, diff --git a/webserver.js b/webserver.js index 991501f6..3a0dc725 100644 --- a/webserver.js +++ b/webserver.js @@ -1289,7 +1289,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Check if the password is the same as a previous one obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) { - if (result == true) { + if (result != 0) { // This is the same password as an older one, request a password change again parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)'); req.session.loginmode = '6'; @@ -1870,6 +1870,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Check a user's old passwords + // Callback: 0=OK, 1=OldPass, 2=CommonPass obj.checkOldUserPasswords = function (domain, user, password, func) { // Check how many old passwords we need to check if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) { @@ -1884,11 +1885,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // If there is no old passwords, exit now. var oldPassCount = 1; if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; } - var oldPassCheckState = { response: false, count: oldPassCount, user: user, func: func }; + var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func }; + + // Test against common passwords if this feature is enabled + // Example of common passwords: 123456789, password123 + if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) { + oldPassCheckState.count++; + require('wildleek')(password).then(function (wild) { + if (wild == true) { oldPassCheckState.response = 2; } + if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); } + }); + } // Try current password require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) { - if ((err == null) && (hash == tag.user.hash)) { tag.response = true; } + if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; } if (--tag.count == 0) { tag.func(tag.response); } }, oldPassCheckState); @@ -1898,7 +1909,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { const oldpassword = user.oldpasswords[i]; // Default strong password hashing (pbkdf2 SHA384) require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) { - if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = true; } + if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; } if (--tag.state.count == 0) { tag.state.func(tag.state.response); } }, { oldPassword: oldpassword, state: oldPassCheckState }); } @@ -1939,9 +1950,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (result == true) { // Check if the new password is allowed, only do this if this feature is enabled. parent.checkOldUserPasswords(domain, user, command.newpass, function (result) { - if (result == true) { + if (result == 1) { parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.'); if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } + } else if (result == 2) { + parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.'); + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } } else { // Update the password require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {