Added CAPTCHA option when creating new accounts on login screen.

Ylian Saint-Hilaire committed Jul 11, 2022 at 14:35 UTC 04fb1f2bf04a41c8cfe23ae94c30658691b1a157
8 files changed +102 -22
authenticode.js
+2 -2
@@ -1235,7 +1235,7 @@ function createAuthenticodeHandler(path) {
1235 if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); }
1236 if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); }
1237 if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); }
1238 - if (hashOid == null) { func(false); return; };
1238 + if (hashOid == null) { func('Invalid signing hash: ' + args.hash); return; };
1239
1240 // Create the signature block
1241 var xp7 = forge.pkcs7.createSignedData();
@@ -1453,7 +1453,7 @@ function createAuthenticodeHandler(path) {
1453 // Open the output file
1454 var output = null;
1455 try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
1456 - if (output == null) { func(false); return; }
1456 + if (output == null) { func('Unable to open output file: ' + args.out); return; }
1457 var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize;
1458
1459 // Compute pre-header length and copy that to the new file
meshcentral-config-schema.json
+1
@@ -343,6 +343,7 @@
343 "ipkvm": { "type": "boolean", "default": false, "description": "Set to true to enable IP KVM device support in this domain." },
344 "minify": { "type": "boolean", "default": false, "description": "When enabled, the server will send reduced sided web pages." },
345 "newAccounts": { "type": "boolean", "default": false, "description": "When set to true, allow new user accounts to be created from the login page." },
346 + "newAccountsPass": { "type": "string", "default": null, "description": "When set this password will be required in order to create a new account from the login screen." },
347 "newAccountsUserGroups": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
348 "userNameIsEmail": { "type": "boolean", "default": false, "description": "When enabled, the username of each account is also the email address of the account." },
349 "newAccountEmailDomains": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
meshcentral.js
+4
@@ -3675,6 +3675,7 @@ function mainStart() {
3675 var wildleek = false;
3676 var nodemailer = false;
3677 var sendgrid = false;
3678 + var captcha = false;
3679 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; }
3680 if (domainCount == 0) { allsspi = false; }
3681 for (var i in config.domains) {
@@ -3697,6 +3698,7 @@ function mainStart() {
3698 }
3699 if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
3700 if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
3701 + if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
3702 }
3703
3704 // Build the list of required modules
@@ -3705,6 +3707,8 @@ function mainStart() {
3707 if (ldap == true) { modules.push('ldapauth-fork'); }
3708 if (ssh == true) { if (nodeVersion < 11) { addServerWarning('MeshCentral SSH support requires NodeJS 11 or higher.', 1); } else { modules.push('ssh2'); } }
3709 if (passport != null) { modules.push(...passport); }
3710 + if (captcha == true) { modules.push('svg-captcha'); }
3711 +
3712 if (sessionRecording == true) { modules.push('image-size'); } // Need to get the remote desktop JPEG sizes to index the recodring file.
3713 if (config.letsencrypt != null) { modules.push('acme-client'); } // Add acme-client module
3714 if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules
mpsserver.js
+1 -1
@@ -58,7 +58,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
58 obj.server.listen(args.mpsport, args.mpsportbind, function () {
59 console.log("MeshCentral Intel(R) AMT server running on " + certificates.AmtMpsName + ":" + args.mpsport + ((args.mpsaliasport != null) ? (", alias port " + args.mpsaliasport) : "") + ".");
60 obj.parent.authLog('mps', 'Server listening on ' + ((args.mpsportbind != null) ? args.mpsportbind : '0.0.0.0') + ' port ' + args.mpsport + '.');
61 - }).on("error", function (err) { console.error("ERROR: MeshCentral Intel(R) AMT server port " + args.mpsport + " is not available."); if (args.exactports) { process.exit(); } });
61 + }).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(); } });
62
63 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 + '.'); } });
64 }
package.json
+12 -2
@@ -37,6 +37,8 @@
37 "sample-config-advanced.json"
38 ],
39 "dependencies": {
40 + "@crowdsec/express-bouncer": "^0.1.0",
41 + "@yetzt/nedb": "^1.8.0",
42 "archiver": "^5.3.1",
43 "body-parser": "^1.19.0",
44 "cbor": "~5.2.0",
@@ -45,13 +47,21 @@
47 "express": "^4.17.0",
48 "express-handlebars": "^5.3.5",
49 "express-ws": "^4.0.0",
50 + "image-size": "^1.0.1",
51 "ipcheck": "^0.1.0",
52 + "loadavg-windows": "^1.1.1",
53 "minimist": "^1.2.5",
54 "multiparty": "^4.2.1",
51 - "@yetzt/nedb": "^1.8.0",
55 "node-forge": "^1.0.0",
56 + "node-windows": "^0.1.4",
57 + "otplib": "^10.2.3",
58 + "pg": "^8.7.1",
59 + "pgtools": "^0.3.2",
60 + "ssh2": "^1.11.0",
61 + "web-push": "^3.5.0",
62 "ws": "^5.2.3",
54 - "yauzl": "^2.10.0"
63 + "yauzl": "^2.10.0",
64 + "yubikeyotp": "^0.2.0"
65 },
66 "engines": {
67 "node": ">=10.0.0"
views/login.handlebars
+17 -3
@@ -122,6 +122,14 @@
122 <td id="nuToken" align=right>Creation Token:</td>
123 <td><input id=anewaccountpass type=password name=anewaccountpass {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(6,event) onkeyup=validateCreate(6,event) /></td>
124 </tr>
125 + <tr id=newAccountCaptchaImg title="CAPTCHA image">
126 + <td></td>
127 + <td colspan=2><img src="{{{newAccountCaptchaImage}}}" loading="lazy" /></td>
128 + </tr>
129 + <tr id=newAccountCaptcha title="Security check">
130 + <td id="nuCaptcha" align=right>Security Check:</td>
131 + <td><input id=anewaccountcaptcha type=text name=anewaccountcaptcha {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(7,event) onkeyup=validateCreate(7,event) /></td>
132 + </tr>
133 <tr>
134 <td colspan=2>
135 <div style=float:right><input id=createButton type=submit value="Create Account" disabled="disabled" /></div>
@@ -131,6 +139,7 @@
139 </table>
140 <hr /><a onclick="return xgo(1,event);" href="#" style=cursor:pointer>Back to login</a>
141 <input id=createformargs name="urlargs" type="hidden" value="" />
142 + <input id=createformcaptcha name="captchaargs" type="hidden" value="{{{newAccountCaptcha}}}" />
143 </form>
144 </div>
145 <div id=resetpanel style="display:none">
@@ -311,6 +320,7 @@
320 var loginMode = '{{{loginmode}}}';
321 var newAccount = '{{{newAccount}}}';
322 var newAccountPass = parseInt('{{{newAccountPass}}}');
323 + var newAccountCaptcha = '{{{newAccountCaptcha}}}';
324 var emailCheck = '{{{emailcheck}}}';
325 var passRequirements = '{{{passRequirements}}}';
326 var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
@@ -335,7 +345,7 @@
345 var i;
346 var messageid = parseInt('{{{messageid}}}');
347 var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS sent."];
338 - 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."];
348 + 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."];
349 if (messageid > 0) {
350 var msg = '';
351 if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
@@ -433,6 +443,8 @@
443 QV('newAccountDiv', (newAccount === '1') || (newAccount === 'true')); // If new accounts are not allowed, don't display the new account link.
444 if ((passhint != null) && (passhint.length > 0)) { QV('showPassHintLink', true); }
445 QV('newAccountPass', (newAccountPass == 1));
446 + QV('newAccountCaptcha', (newAccountCaptcha != ''));
447 + QV('newAccountCaptchaImg', (newAccountCaptcha != ''));
448 QV('resetAccountDiv', (emailCheck == 'true'));
449 QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1));
450
@@ -627,6 +639,7 @@
639 var pass1ok = (Q('apassword1').value.length > 0);
640 var pass2ok = (Q('apassword2').value.length > 0) && (Q('apassword2').value == Q('apassword1').value);
641 var newAccOk = (newAccountPass == 0) || (Q('anewaccountpass').value.length > 0);
642 + var newCaptchaOk = (newAccountCaptcha == '') || (Q('anewaccountcaptcha').value.length > 0);
643 var ok = (userok && emailok && pass1ok && pass2ok && newAccOk);
644
645 // Color the fields
@@ -635,6 +648,7 @@
648 QS('nuPass1').color = pass1ok ? 'black' : '#7b241c';
649 QS('nuPass2').color = pass2ok ? 'black' : '#7b241c';
650 QS('nuToken').color = newAccOk ? 'black' : '#7b241c';
651 + QS('nuCaptcha').color = newCaptchaOk ? 'black' : '#7b241c';
652
653 if (Q('apassword1').value == '') {
654 QH('passWarning', '');
@@ -663,13 +677,13 @@
677 }
678 }
679 if ((e != null) && (e.keyCode == 13)) {
666 -
680 if ((box == 1) && userok) { Q('aemail').focus(); }
681 if ((box == 2) && emailok) { Q('apassword1').focus(); }
682 if ((box == 3) && pass1ok) { Q('apassword2').focus(); }
683 if ((box == 4) && pass2ok) { if (passRequirements.hint === true) { Q('apasswordhint').focus(); } else { box = 5; } }
684 if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { box = 6; } }
672 - if (box == 6) { Q('createButton').click(); }
685 + if (box == 6) { if (newAccountCaptcha != '') { Q('anewaccountcaptcha').focus(); } else { box = 7; } }
686 + if (box == 7) { Q('createButton').click(); }
687 }
688 if (e != null) { haltEvent(e); }
689 QE('createButton', ok);
views/login2.handlebars
+17 -3
@@ -144,6 +144,14 @@
144 <td id="nuToken" align=right>Creation Token:</td>
145 <td><input id=anewaccountpass type=password name=anewaccountpass {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(6,event) onkeyup=validateCreate(6,event) /></td>
146 </tr>
147 + <tr id=newAccountCaptchaImg title="CAPTCHA image">
148 + <td></td>
149 + <td colspan=2><img src="{{{newAccountCaptchaImage}}}" loading="lazy" /></td>
150 + </tr>
151 + <tr id=newAccountCaptcha title="Security check">
152 + <td id="nuCaptcha" align=right>Security Check:</td>
153 + <td><input id=anewaccountcaptcha type=text name=anewaccountcaptcha {{{autocomplete}}}=off maxlength=256 onkeydown=haltReturn(event) onchange=validateCreate(7,event) onkeyup=validateCreate(7,event) /></td>
154 + </tr>
155 <tr>
156 <td colspan=2>
157 <div style=float:right><input id=createButton type="button" onclick="submitButtonClicked('createpanelform')" value="Create Account" disabled="disabled" /></div>
@@ -153,6 +161,7 @@
161 </table>
162 <hr /><a onclick="return xgo(1,event);" href="#" style=cursor:pointer>Back to login</a>
163 <input id=createformargs name="urlargs" type="hidden" value="" />
164 + <input id=createformcaptcha name="captchaargs" type="hidden" value="{{{newAccountCaptcha}}}" />
165 </form>
166 </div>
167 <div id=resetpanel style="display:none">
@@ -368,6 +377,7 @@
377 var loginMode = '{{{loginmode}}}';
378 var newAccount = '{{{newAccount}}}';
379 var newAccountPass = parseInt('{{{newAccountPass}}}');
380 + var newAccountCaptcha = '{{{newAccountCaptcha}}}';
381 var emailCheck = '{{{emailcheck}}}';
382 var passRequirements = '{{{passRequirements}}}';
383 var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
@@ -405,7 +415,7 @@
415 var i;
416 var messageid = parseInt('{{{messageid}}}');
417 var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS sent.", "Sending notification..."];
408 - 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."];
418 + 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."];
419 if (messageid > 0) {
420 var msg = '';
421 if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
@@ -487,6 +497,8 @@
497 QV('newAccountDiv', (newAccount === '1') || (newAccount === 'true')); // If new accounts are not allowed, don't display the new account link.
498 if ((passhint != null) && (passhint.length > 0)) { QV('showPassHintLink', true); }
499 QV('newAccountPass', (newAccountPass == 1));
500 + QV('newAccountCaptcha', (newAccountCaptcha != ''));
501 + QV('newAccountCaptchaImg', (newAccountCaptcha != ''));
502 QV('resetAccountDiv', (emailCheck == 'true'));
503 QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1));
504
@@ -712,6 +724,7 @@
724 var pass1ok = (Q('apassword1').value.length > 0);
725 var pass2ok = (Q('apassword2').value.length > 0) && (Q('apassword2').value == Q('apassword1').value);
726 var newAccOk = (newAccountPass == 0) || (Q('anewaccountpass').value.length > 0);
727 + var newCaptchaOk = (newAccountCaptcha == '') || (Q('anewaccountcaptcha').value.length > 0);
728 var ok = (userok && emailok && pass1ok && pass2ok && newAccOk);
729
730 // Color the fields
@@ -720,6 +733,7 @@
733 QS('nuPass1').color = pass1ok ? 'black' : '#7b241c';
734 QS('nuPass2').color = pass2ok ? 'black' : '#7b241c';
735 QS('nuToken').color = newAccOk ? 'black' : '#7b241c';
736 + QS('nuCaptcha').color = newCaptchaOk ? 'black' : '#7b241c';
737
738 if (Q('apassword1').value == '') {
739 QH('passWarning', '');
@@ -748,13 +762,13 @@
762 }
763 }
764 if ((e != null) && (e.keyCode == 13)) {
751 -
765 if ((box == 1) && userok) { Q('aemail').focus(); }
766 if ((box == 2) && emailok) { Q('apassword1').focus(); }
767 if ((box == 3) && pass1ok) { Q('apassword2').focus(); }
768 if ((box == 4) && pass2ok) { if (passRequirements.hint === true) { Q('apasswordhint').focus(); } else { box = 5; } }
769 if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { box = 6; } }
757 - if (box == 6) { Q('createButton').click(); }
770 + if (box == 6) { if (newAccountCaptcha != '') { Q('anewaccountcaptcha').focus(); } else { box = 7; } }
771 + if (box == 7) { Q('createButton').click(); }
772 }
773 if (e != null) { haltEvent(e); }
774 QE('createButton', ok);
webserver.js
+48 -11
@@ -1359,6 +1359,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1359 // If the email is the username, set this here.
1360 if (domain.usernameisemail) { req.body.username = req.body.email; }
1361
1362 + // Check if there is domain.newAccountToken, check if supplied token is valid
1363 + if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
1364 + parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
1365 + req.session.loginmode = 2;
1366 + req.session.messageid = 103; // Invalid account creation token.
1367 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1368 + return;
1369 + }
1370 +
1371 + // If needed, check the new account creation CAPTCHA
1372 + if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
1373 + const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
1374 + if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
1375 + req.session.loginmode = 2;
1376 + req.session.messageid = 117; // Invalid security check
1377 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1378 + return;
1379 + }
1380 + }
1381 +
1382 // Accounts that start with ~ are not allowed
1383 if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
1384 parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
@@ -1423,14 +1443,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1443 req.session.messageid = 102; // Existing account with this email address.
1444 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1445 } else {
1426 - // Check if there is domain.newAccountToken, check if supplied token is valid
1427 - if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
1428 - parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
1429 - req.session.loginmode = 2;
1430 - req.session.messageid = 103; // Invalid account creation token.
1431 - if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1432 - return;
1433 - }
1446 // Check if user exists
1447 if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
1448 parent.debug('web', 'handleCreateAccountRequest: Username already exists');
@@ -3054,20 +3066,29 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3066 twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
3067 }
3068
3069 + // Setup CAPTCHA if needed
3070 + var newAccountCaptcha = '', newAccountCaptchaImage = '';
3071 + if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
3072 + newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
3073 + newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
3074 + }
3075 +
3076 // Render the login page
3077 render(req, res,
3078 getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain),
3079 getRenderArgs({
3080 loginmode: loginmode,
3081 rootCertLink: getRootCertLink(domain),
3063 - newAccount: newAccountsAllowed,
3064 - newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1),
3082 + newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
3083 + newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
3084 + newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
3085 + newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
3086 serverDnsName: obj.getWebServerName(domain),
3087 serverPublicPort: httpsPort,
3088 passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
3089 emailcheck: emailcheck,
3090 features: features,
3070 - sessiontime: (args.sessiontime) ? args.sessiontime : 60,
3091 + sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
3092 passRequirements: passRequirements,
3093 customui: customui,
3094 footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
@@ -3195,6 +3216,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3216 }
3217 }
3218
3219 + // Handle new account Captcha GET
3220 + function handleNewAccountCaptchaRequest(req, res) {
3221 + const domain = checkUserIpAddress(req, res);
3222 + if (domain == null) { return; }
3223 + if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
3224 + const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
3225 + if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
3226 + res.type('svg');
3227 + res.status(200).end(require('svg-captcha')(c.captcha, {}));
3228 + }
3229 +
3230 // Handle Captcha GET
3231 function handleCaptchaGetRequest(req, res) {
3232 const domain = checkUserIpAddress(req, res);
@@ -6104,6 +6136,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6136 obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
6137 }
6138
6139 + // New account CAPTCHA request
6140 + if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
6141 + obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
6142 + }
6143 +
6144 // Check CrowdSec Bounser if configured
6145 if (parent.crowdSecBounser != null) {
6146 obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);