From ab375577b2709712daf7ccbcc85e32996d10703a Mon Sep 17 00:00:00 2001 From: Rupeshiya Date: Sun, 23 Aug 2020 12:27:27 +0530 Subject: [PATCH] fix security flaws --- app.js | 22 +++++++ app/controllers/auth.js | 4 +- app/controllers/user.js | 25 +++++++- app/middleware/rateLimiter.js | 81 ++++++++++++++++++++++++ app/middleware/sanitise.js | 7 +++ package-lock.json | 115 ++++++++++++++++++++++++++++++++-- package.json | 6 +- 7 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 app/middleware/rateLimiter.js create mode 100644 app/middleware/sanitise.js diff --git a/app.js b/app.js index 107ea58..d2b8f18 100644 --- a/app.js +++ b/app.js @@ -8,7 +8,11 @@ const socket = require('socket.io') const multer = require('multer') const bodyParser = require('body-parser') const cors = require('cors') +const helmet = require('helmet') +const hpp = require('hpp') var winston = require('./config/winston') +const rateLimiter = require('./app/middleware/rateLimiter') +const sanitizer = require('./app/middleware/sanitise') const fileConstants = require('./config/fileHandlingConstants') const indexRouter = require('./app/routes/index') @@ -31,6 +35,7 @@ const server = require('http').Server(app) app.use(cors()) app.use(bodyParser.json({ limit: '200mb' })) +app.use(cookieParser()) app.use(bodyParser.urlencoded(fileConstants.fileParameters)) const memoryStorage = multer.memoryStorage() @@ -72,6 +77,23 @@ app.use((req, res, next) => { next() }) +// TO PREVENT DOS ATTACK AND RATE LIMITER +app.use(rateLimiter.customRateLimiter) + +// TO PREVENT XSS ATTACK +app.use(sanitizer.cleanBody) +app.use(helmet()) + +// TO PREVENT CLICK JACKING +app.use((req, res, next) => { + res.append('X-Frame-Options', 'Deny') + res.set('Content-Security-Policy', "frame-ancestors 'none';") + next() +}) + +// TO PREVENT THE QUERY PARAMETER POLLUTION +app.use(hpp()) + app.use('/notification', notificationRouter) app.use('/', indexRouter) app.use('/auth', authRouter) diff --git a/app/controllers/auth.js b/app/controllers/auth.js index a471a58..cd0138f 100644 --- a/app/controllers/auth.js +++ b/app/controllers/auth.js @@ -4,8 +4,8 @@ const activityHelper = require('../utils/activity-helper') module.exports = { authenticateUser: async (req, res, next) => { - const email = req.body.email - const password = req.body.password + const email = escape(req.body.email) + const password = escape(req.body.password) try { const user = await User.findByCredentials(email, password) const token = await user.generateAuthToken() diff --git a/app/controllers/user.js b/app/controllers/user.js index c0b9b6e..fc303c3 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -45,7 +45,8 @@ module.exports = { // create redis db for activity for the user const activity = new Activity({ userId: data._id }) await activity.save() - + // hide password + user.password = undefined return res.status(HttpStatus.CREATED).json({ user: user, token: token }) } catch (error) { return res.status(HttpStatus.NOT_ACCEPTABLE).json({ error: error }) @@ -81,6 +82,9 @@ module.exports = { if (!user) { return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such user exist!' }) } + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } catch (error) { HANDLER.handleError(res, error) @@ -94,6 +98,7 @@ module.exports = { 'phone', 'info', 'about', + 'socialMedia', 'isDeactivated' ] // added control as per org settings @@ -118,6 +123,9 @@ module.exports = { user[update] = req.body[update] }) await user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ data: user }) } catch (error) { return res.status(HttpStatus.BAD_REQUEST).json({ error }) @@ -308,6 +316,9 @@ module.exports = { .populate('followers', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .populate('blocked', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .exec() + // hide password and tokens + userData.password = undefined + userData.tokens = [] return res.status(HttpStatus.OK).json({ user: userData }) } catch (error) { HANDLER.handleError(res, error) @@ -358,6 +369,9 @@ module.exports = { .populate('followers', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .populate('blocked', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .exec() + // hide password and tokens + userData.password = undefined + userData.tokens = [] return res.status(HttpStatus.OK).json({ user: userData }) } catch (error) { HANDLER.handleError(res, error) @@ -404,6 +418,9 @@ module.exports = { if (unblockIndex !== -1) { user.blocked.splice(unblockIndex, 1) await user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } return res.status(HttpStatus.NOT_FOUND).json({ user }) @@ -441,6 +458,9 @@ module.exports = { } user.isRemoved = true await user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } catch (error) { HANDLER.handleError(res, error) @@ -451,6 +471,9 @@ module.exports = { try { req.user.isActivated = !req.user.isActivated const user = await req.user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } catch (error) { HANDLER.handleError(error) diff --git a/app/middleware/rateLimiter.js b/app/middleware/rateLimiter.js new file mode 100644 index 0000000..8bf4121 --- /dev/null +++ b/app/middleware/rateLimiter.js @@ -0,0 +1,81 @@ +const redis = require('../../config/redis') +const redisClient = redis.redisClient +const moment = require('moment') +const WINDOW_SIZE_IN_HOURS = 24 +const MAX_WINDOW_REQUEST_COUNT = 100 +const WINDOW_LOG_INTERVAL_IN_HOURS = 1 + +module.exports = { + customRateLimiter: (req, res, next) => { + try { + // check if redis exists + if (!redisClient) { + throw new Error('RedisClient not found on the server') + } + // if exists check if request made earlier from same ip + redisClient.get(req.ip, (err, reply) => { + if (err) { + console.log('Error in fetching data from redis', err) + } + const currentRequestTime = moment() + // if no reply from redis then store the users request to the server in redis + if (reply === null) { + const newRecord = [] + const info = { + requestTimeStamp: currentRequestTime.unix(), + requestCount: 1 + } + newRecord.unshift(info) + // set to redis => ip => [{ requestTimeStamp, requestCount }] + redisClient.set(req.ip, JSON.stringify(newRecord)) + next() + } + + // if record is found, parse it's value and calculate number of requests users has made within the last window + const data = JSON.parse(reply) + + const windowStartTimestamp = moment() + .subtract(WINDOW_SIZE_IN_HOURS, 'hours') + .unix() + + const requestsWithinWindow = data.filter(entry => { + return entry.requestTimeStamp > windowStartTimestamp + }) + + const totalWindowRequestsCount = requestsWithinWindow.reduce((accumulator, entry) => { + return accumulator + entry.requestCount + }, 0) + + // if number of requests made is greater than or equal to the desired maximum, return error + if (totalWindowRequestsCount >= MAX_WINDOW_REQUEST_COUNT) { + return res.status(429).json({ + error: `You have exceeded the ${MAX_WINDOW_REQUEST_COUNT} requests in ${WINDOW_SIZE_IN_HOURS} hrs limit!` + }) + } else { + // if number of requests made is less than allowed maximum, log new entry + const lastRequestLog = data[data.length - 1] + const potentialCurrentWindowIntervalStartTimeStamp = currentRequestTime + .subtract(WINDOW_LOG_INTERVAL_IN_HOURS, 'hours') + .unix() + + // if interval has not passed since last request log, increment counter + if (lastRequestLog.requestTimeStamp > potentialCurrentWindowIntervalStartTimeStamp) { + lastRequestLog.requestCount++ + data[data.length - 1] = lastRequestLog + } else { + // if interval has passed, log new entry for current user and timestamp + data.unshift({ + requestTimeStamp: currentRequestTime.unix(), + requestCount: 1 + }) + } + redisClient.set(req.ip, JSON.stringify(data)) + next() + } + }) + } catch (error) { + console.log('Error in rateLimiter', error) + next(error) + } + } +} diff --git a/app/middleware/sanitise.js b/app/middleware/sanitise.js new file mode 100644 index 0000000..980ae45 --- /dev/null +++ b/app/middleware/sanitise.js @@ -0,0 +1,7 @@ +const sanitize = require('mongo-sanitize') +module.exports = { + cleanBody: (req, res, next) => { + req.body = sanitize(req.body) + next() + } +} diff --git a/package-lock.json b/package-lock.json index 3329fa0..47ead99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1696,12 +1696,19 @@ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, "cookie-parser": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", - "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", + "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", "requires": { - "cookie": "0.3.1", + "cookie": "0.4.0", "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + } } }, "cookie-signature": { @@ -1766,6 +1773,16 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "requires": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + } + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -1781,6 +1798,51 @@ "cssom": "0.3.x" } }, + "csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "requires": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -4218,12 +4280,26 @@ } } }, + "helmet": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.1.0.tgz", + "integrity": "sha512-KWy75fYN8hOG2Rhl8e5B3WhOzb0by1boQum85TiddIE9iu6gV+TXbUjVC17wfej0o/ZUpqB9kxM0NFCZRMzf+Q==" + }, "hosted-git-info": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", "dev": true }, + "hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "requires": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + } + }, "html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -5699,8 +5775,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.defaults": { "version": "4.2.0", @@ -5974,6 +6049,11 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" }, + "mongo-sanitize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mongo-sanitize/-/mongo-sanitize-1.1.0.tgz", + "integrity": "sha512-6gB9AiJD+om2eZLxaPKIP5Q8P3Fr+s+17rVWso7hU0+MAzmIvIMlgTYuyvalDLTtE/p0gczcvJ8A3pbN1XmQ/A==" + }, "mongodb": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.3.tgz", @@ -6797,6 +6877,11 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7157,6 +7242,11 @@ "glob": "^7.1.3" } }, + "rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -8244,6 +8334,11 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -8315,6 +8410,14 @@ } } }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "undefsafe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", diff --git a/package.json b/package.json index 72af423..6e78e43 100644 --- a/package.json +++ b/package.json @@ -15,18 +15,22 @@ "aws-sdk": "^2.691.0", "bcrypt": "^3.0.6", "body-parser": "^1.19.0", - "cookie-parser": "~1.4.4", + "cookie-parser": "^1.4.5", "cors": "^2.8.5", "crypto": "^1.0.1", + "csurf": "^1.11.0", "debug": "~2.6.9", "dotenv": "^8.2.0", "ejs": "~2.6.1", "express": "^4.16.4", "googleapis": "^56.0.0", + "helmet": "^4.1.0", + "hpp": "^0.2.3", "http-status-codes": "^1.4.0", "ioredis": "^4.17.3", "jsonwebtoken": "^8.5.1", "moment": "^2.27.0", + "mongo-sanitize": "^1.1.0", "mongoose": "^5.7.7", "morgan": "^1.9.1", "multer": "^1.4.2",