diff --git a/common/throttle.js b/common/throttle.js new file mode 100644 index 0000000..efd0e8e --- /dev/null +++ b/common/throttle.js @@ -0,0 +1,31 @@ +let CronJob = require('cron').CronJob; + +let emails = []; + +function throttle(email) { + emails[email] = new Date(); +} + +function isThrottled(email) { + if (emails[email] === undefined) { + return false; + } + + if ( (emails[email] - new Date()) / 1000 > 3) { //3 seconds + return false; + } + + return true; +} + +//clear the memory once a day +let job = new CronJob('0 7 * * * *', () => { + emails = []; +}); + +job.start(); + +module.exports = { + throttle: throttle, + isThrottled: isThrottled +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 502f626..794f104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2069,6 +2069,14 @@ "gud": "^1.0.0" } }, + "cron": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/cron/-/cron-1.7.1.tgz", + "integrity": "sha512-gmMB/pJcqUVs/NklR1sCGlNYM7TizEw+1gebz20BMc/8bTm/r7QUp3ZPSPlG8Z5XRlvb7qhjEjq/+bdIfUCL2A==", + "requires": { + "moment-timezone": "^0.5.x" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -4552,6 +4560,19 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.25", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz", + "integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==", + "requires": { + "moment": ">= 2.9.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/package.json b/package.json index f7865f5..2b66c7a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "babel-loader": "^8.0.5", "bcrypt": "^3.0.6", "body-parser": "^1.19.0", + "cron": "^1.7.1", "dotenv": "^8.0.0", "express": "^4.16.4", "forever": "^1.0.0", diff --git a/server/accounts.js b/server/accounts.js index 3b0becc..1dcc90a 100644 --- a/server/accounts.js +++ b/server/accounts.js @@ -8,6 +8,7 @@ let sendmail = require('sendmail')(); //utilities let { validateEmail } = require('../common/utilities.js'); +let { throttle, isThrottled } = require('../common/throttle.js'); function signup(connection) { return (req, res) => { @@ -56,6 +57,15 @@ function signup(connection) { connection.query(query, [fields.email, fields.username, salt, hash, rand], (err) => { if (err) throw err; + //prevent too many clicks + if (isThrottled(fields.email)) { + res.status(400).write('signup throttled'); + res.end(); + return; + } + + throttle(fields.email); + //build the verification email let addr = `http://${process.env.WEB_ADDRESS}/verify?email=${fields.email}&verify=${rand}`; let msg = 'Hello! Please visit the following address to verify your account: '; @@ -304,6 +314,15 @@ function passwordRecover(connection) { let msg = 'Hello! Please visit the following address to set a new password (if you didn\'t request a password recovery, ignore this email): '; let msgHtml = `
${msg}${addr}
`; + //prevent too many clicks + if (isThrottled(fields.email)) { + res.status(400).write('recover throttled'); + res.end(); + return; + } + + throttle(fields.email); + //send the verification email sendmail({ from: `passwordrecover@${process.env.WEB_ADDRESS}`,