Added CAPTCHA option when creating new accounts on login screen.

This commit is contained in:
Ylian Saint-Hilaire 2022-07-11 14:35:05 -07:00
parent 4382899468
commit 04fb1f2bf0
8 changed files with 102 additions and 22 deletions

View File

@ -1235,7 +1235,7 @@ function createAuthenticodeHandler(path) {
if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); }
if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); }
if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); }
if (hashOid == null) { func(false); return; };
if (hashOid == null) { func('Invalid signing hash: ' + args.hash); return; };
// Create the signature block
var xp7 = forge.pkcs7.createSignedData();
@ -1453,7 +1453,7 @@ function createAuthenticodeHandler(path) {
// Open the output file
var output = null;
try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
if (output == null) { func(false); return; }
if (output == null) { func('Unable to open output file: ' + args.out); return; }
var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize;
// Compute pre-header length and copy that to the new file

View File

@ -343,6 +343,7 @@
"ipkvm": { "type": "boolean", "default": false, "description": "Set to true to enable IP KVM device support in this domain." },
"minify": { "type": "boolean", "default": false, "description": "When enabled, the server will send reduced sided web pages." },
"newAccounts": { "type": "boolean", "default": false, "description": "When set to true, allow new user accounts to be created from the login page." },
"newAccountsPass": { "type": "string", "default": null, "description": "When set this password will be required in order to create a new account from the login screen." },
"newAccountsUserGroups": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
"userNameIsEmail": { "type": "boolean", "default": false, "description": "When enabled, the username of each account is also the email address of the account." },
"newAccountEmailDomains": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },

View File

@ -3675,6 +3675,7 @@ function mainStart() {
var wildleek = false;
var nodemailer = false;
var sendgrid = false;
var captcha = 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) {
@ -3697,6 +3698,7 @@ function mainStart() {
}
if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
}
// Build the list of required modules
@ -3705,6 +3707,8 @@ function mainStart() {
if (ldap == true) { modules.push('ldapauth-fork'); }
if (ssh == true) { if (nodeVersion < 11) { addServerWarning('MeshCentral SSH support requires NodeJS 11 or higher.', 1); } else { modules.push('ssh2'); } }
if (passport != null) { modules.push(...passport); }
if (captcha == true) { modules.push('svg-captcha'); }
if (sessionRecording == true) { modules.push('image-size'); } // Need to get the remote desktop JPEG sizes to index the recodring file.
if (config.letsencrypt != null) { modules.push('acme-client'); } // Add acme-client module
if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules

View File

@ -58,7 +58,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
obj.server.listen(args.mpsport, args.mpsportbind, function () {
console.log("MeshCentral Intel(R) AMT server running on " + certificates.AmtMpsName + ":" + args.mpsport + ((args.mpsaliasport != null) ? (", alias port " + args.mpsaliasport) : "") + ".");
obj.parent.authLog('mps', 'Server listening on ' + ((args.mpsportbind != null) ? args.mpsportbind : '0.0.0.0') + ' port ' + args.mpsport + '.');
}).on("error", function (err) { console.error("ERROR: MeshCentral Intel(R) AMT server port " + args.mpsport + " is not available."); if (args.exactports) { process.exit(); } });
}).on("error", function (err) { console.error("ERROR: MeshCentral Intel(R) AMT server port " + args.mpsport + " is not available. Check if the MeshCentral is already running."); if (args.exactports) { process.exit(); } });
obj.server.on('tlsClientError', function (err, tlssocket) { if (args.mpsdebug) { var remoteAddress = tlssocket.remoteAddress; if (tlssocket.remoteFamily == 'IPv6') { remoteAddress = '[' + remoteAddress + ']'; } console.log('MPS:Invalid TLS connection from ' + remoteAddress + ':' + tlssocket.remotePort + '.'); } });
}

View File

@ -37,6 +37,8 @@
"sample-config-advanced.json"
],
"dependencies": {
"@crowdsec/express-bouncer": "^0.1.0",
"@yetzt/nedb": "^1.8.0",
"archiver": "^5.3.1",
"body-parser": "^1.19.0",
"cbor": "~5.2.0",
@ -45,13 +47,21 @@
"express": "^4.17.0",
"express-handlebars": "^5.3.5",
"express-ws": "^4.0.0",
"image-size": "^1.0.1",
"ipcheck": "^0.1.0",
"loadavg-windows": "^1.1.1",
"minimist": "^1.2.5",
"multiparty": "^4.2.1",
"@yetzt/nedb": "^1.8.0",
"node-forge": "^1.0.0",
"node-windows": "^0.1.4",
"otplib": "^10.2.3",
"pg": "^8.7.1",
"pgtools": "^0.3.2",
"ssh2": "^1.11.0",
"web-push": "^3.5.0",
"ws": "^5.2.3",
"yauzl": "^2.10.0"
"yauzl": "^2.10.0",
"yubikeyotp": "^0.2.0"
},
"engines": {
"node": ">=10.0.0"

View File

@ -122,6 +122,14 @@
<td id="nuToken" align=right>Creation Token:</td>
<td><input id=anewaccountpass type=password name=anewaccountpass {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(6,event) onkeyup=validateCreate(6,event) /></td>
</tr>
<tr id=newAccountCaptchaImg title="CAPTCHA image">
<td></td>
<td colspan=2><img src="{{{newAccountCaptchaImage}}}" loading="lazy" /></td>
</tr>
<tr id=newAccountCaptcha title="Security check">
<td id="nuCaptcha" align=right>Security Check:</td>
<td><input id=anewaccountcaptcha type=text name=anewaccountcaptcha {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(7,event) onkeyup=validateCreate(7,event) /></td>
</tr>
<tr>
<td colspan=2>
<div style=float:right><input id=createButton type=submit value="Create Account" disabled="disabled" /></div>
@ -131,6 +139,7 @@
</table>
<hr /><a onclick="return xgo(1,event);" href="#" style=cursor:pointer>Back to login</a>
<input id=createformargs name="urlargs" type="hidden" value="" />
<input id=createformcaptcha name="captchaargs" type="hidden" value="{{{newAccountCaptcha}}}" />
</form>
</div>
<div id=resetpanel style="display:none">
@ -311,6 +320,7 @@
var loginMode = '{{{loginmode}}}';
var newAccount = '{{{newAccount}}}';
var newAccountPass = parseInt('{{{newAccountPass}}}');
var newAccountCaptcha = '{{{newAccountCaptcha}}}';
var emailCheck = '{{{emailcheck}}}';
var passRequirements = '{{{passRequirements}}}';
var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
@ -335,7 +345,7 @@
var i;
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS 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.", "IP address blocked, try again later.", "Server under maintenance."];
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.", "Server under maintenance.", "Unable to send device notification.", "Invalid security check."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
@ -433,6 +443,8 @@
QV('newAccountDiv', (newAccount === '1') || (newAccount === 'true')); // If new accounts are not allowed, don't display the new account link.
if ((passhint != null) && (passhint.length > 0)) { QV('showPassHintLink', true); }
QV('newAccountPass', (newAccountPass == 1));
QV('newAccountCaptcha', (newAccountCaptcha != ''));
QV('newAccountCaptchaImg', (newAccountCaptcha != ''));
QV('resetAccountDiv', (emailCheck == 'true'));
QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1));
@ -627,6 +639,7 @@
var pass1ok = (Q('apassword1').value.length > 0);
var pass2ok = (Q('apassword2').value.length > 0) && (Q('apassword2').value == Q('apassword1').value);
var newAccOk = (newAccountPass == 0) || (Q('anewaccountpass').value.length > 0);
var newCaptchaOk = (newAccountCaptcha == '') || (Q('anewaccountcaptcha').value.length > 0);
var ok = (userok && emailok && pass1ok && pass2ok && newAccOk);
// Color the fields
@ -635,6 +648,7 @@
QS('nuPass1').color = pass1ok ? 'black' : '#7b241c';
QS('nuPass2').color = pass2ok ? 'black' : '#7b241c';
QS('nuToken').color = newAccOk ? 'black' : '#7b241c';
QS('nuCaptcha').color = newCaptchaOk ? 'black' : '#7b241c';
if (Q('apassword1').value == '') {
QH('passWarning', '');
@ -663,13 +677,13 @@
}
}
if ((e != null) && (e.keyCode == 13)) {
if ((box == 1) && userok) { Q('aemail').focus(); }
if ((box == 2) && emailok) { Q('apassword1').focus(); }
if ((box == 3) && pass1ok) { Q('apassword2').focus(); }
if ((box == 4) && pass2ok) { if (passRequirements.hint === true) { Q('apasswordhint').focus(); } else { box = 5; } }
if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { box = 6; } }
if (box == 6) { Q('createButton').click(); }
if (box == 6) { if (newAccountCaptcha != '') { Q('anewaccountcaptcha').focus(); } else { box = 7; } }
if (box == 7) { Q('createButton').click(); }
}
if (e != null) { haltEvent(e); }
QE('createButton', ok);

View File

@ -144,6 +144,14 @@
<td id="nuToken" align=right>Creation Token:</td>
<td><input id=anewaccountpass type=password name=anewaccountpass {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(6,event) onkeyup=validateCreate(6,event) /></td>
</tr>
<tr id=newAccountCaptchaImg title="CAPTCHA image">
<td></td>
<td colspan=2><img src="{{{newAccountCaptchaImage}}}" loading="lazy" /></td>
</tr>
<tr id=newAccountCaptcha title="Security check">
<td id="nuCaptcha" align=right>Security Check:</td>
<td><input id=anewaccountcaptcha type=text name=anewaccountcaptcha {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(7,event) onkeyup=validateCreate(7,event) /></td>
</tr>
<tr>
<td colspan=2>
<div style=float:right><input id=createButton type="button" onclick="submitButtonClicked('createpanelform')" value="Create Account" disabled="disabled" /></div>
@ -153,6 +161,7 @@
</table>
<hr /><a onclick="return xgo(1,event);" href="#" style=cursor:pointer>Back to login</a>
<input id=createformargs name="urlargs" type="hidden" value="" />
<input id=createformcaptcha name="captchaargs" type="hidden" value="{{{newAccountCaptcha}}}" />
</form>
</div>
<div id=resetpanel style="display:none">
@ -368,6 +377,7 @@
var loginMode = '{{{loginmode}}}';
var newAccount = '{{{newAccount}}}';
var newAccountPass = parseInt('{{{newAccountPass}}}');
var newAccountCaptcha = '{{{newAccountCaptcha}}}';
var emailCheck = '{{{emailcheck}}}';
var passRequirements = '{{{passRequirements}}}';
var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
@ -405,7 +415,7 @@
var i;
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS sent.", "Sending notification..."];
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.", "Server under maintenance.", "Unable to send device notification."];
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.", "Server under maintenance.", "Unable to send device notification.", "Invalid security check."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
@ -487,6 +497,8 @@
QV('newAccountDiv', (newAccount === '1') || (newAccount === 'true')); // If new accounts are not allowed, don't display the new account link.
if ((passhint != null) && (passhint.length > 0)) { QV('showPassHintLink', true); }
QV('newAccountPass', (newAccountPass == 1));
QV('newAccountCaptcha', (newAccountCaptcha != ''));
QV('newAccountCaptchaImg', (newAccountCaptcha != ''));
QV('resetAccountDiv', (emailCheck == 'true'));
QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1));
@ -712,6 +724,7 @@
var pass1ok = (Q('apassword1').value.length > 0);
var pass2ok = (Q('apassword2').value.length > 0) && (Q('apassword2').value == Q('apassword1').value);
var newAccOk = (newAccountPass == 0) || (Q('anewaccountpass').value.length > 0);
var newCaptchaOk = (newAccountCaptcha == '') || (Q('anewaccountcaptcha').value.length > 0);
var ok = (userok && emailok && pass1ok && pass2ok && newAccOk);
// Color the fields
@ -720,6 +733,7 @@
QS('nuPass1').color = pass1ok ? 'black' : '#7b241c';
QS('nuPass2').color = pass2ok ? 'black' : '#7b241c';
QS('nuToken').color = newAccOk ? 'black' : '#7b241c';
QS('nuCaptcha').color = newCaptchaOk ? 'black' : '#7b241c';
if (Q('apassword1').value == '') {
QH('passWarning', '');
@ -748,13 +762,13 @@
}
}
if ((e != null) && (e.keyCode == 13)) {
if ((box == 1) && userok) { Q('aemail').focus(); }
if ((box == 2) && emailok) { Q('apassword1').focus(); }
if ((box == 3) && pass1ok) { Q('apassword2').focus(); }
if ((box == 4) && pass2ok) { if (passRequirements.hint === true) { Q('apasswordhint').focus(); } else { box = 5; } }
if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { box = 6; } }
if (box == 6) { Q('createButton').click(); }
if (box == 6) { if (newAccountCaptcha != '') { Q('anewaccountcaptcha').focus(); } else { box = 7; } }
if (box == 7) { Q('createButton').click(); }
}
if (e != null) { haltEvent(e); }
QE('createButton', ok);

View File

@ -1359,6 +1359,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
// If the email is the username, set this here.
if (domain.usernameisemail) { req.body.username = req.body.email; }
// Check if there is domain.newAccountToken, check if supplied token is valid
if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
req.session.loginmode = 2;
req.session.messageid = 103; // Invalid account creation token.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// If needed, check the new account creation CAPTCHA
if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
req.session.loginmode = 2;
req.session.messageid = 117; // Invalid security check
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
}
// Accounts that start with ~ are not allowed
if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
@ -1423,14 +1443,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
req.session.messageid = 102; // Existing account with this email address.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
// Check if there is domain.newAccountToken, check if supplied token is valid
if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
req.session.loginmode = 2;
req.session.messageid = 103; // Invalid account creation token.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Check if user exists
if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
parent.debug('web', 'handleCreateAccountRequest: Username already exists');
@ -3054,20 +3066,29 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
}
// Setup CAPTCHA if needed
var newAccountCaptcha = '', newAccountCaptchaImage = '';
if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
}
// Render the login page
render(req, res,
getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain),
getRenderArgs({
loginmode: loginmode,
rootCertLink: getRootCertLink(domain),
newAccount: newAccountsAllowed,
newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1),
newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
serverDnsName: obj.getWebServerName(domain),
serverPublicPort: httpsPort,
passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
emailcheck: emailcheck,
features: features,
sessiontime: (args.sessiontime) ? args.sessiontime : 60,
sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
passRequirements: passRequirements,
customui: customui,
footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
@ -3195,6 +3216,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
}
}
// Handle new account Captcha GET
function handleNewAccountCaptchaRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
res.type('svg');
res.status(200).end(require('svg-captcha')(c.captcha, {}));
}
// Handle Captcha GET
function handleCaptchaGetRequest(req, res) {
const domain = checkUserIpAddress(req, res);
@ -6104,6 +6136,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
}
// New account CAPTCHA request
if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
}
// Check CrowdSec Bounser if configured
if (parent.crowdSecBounser != null) {
obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);