From 933b9af89940a528ec815bb050fde5ede799c39e Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Thu, 6 Jan 2022 15:05:45 -0800 Subject: [PATCH] Added user session destruction on logout for improved security. --- package.json | 16 ++++++++++++++-- webserver.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7f597e58..10e89daa 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "sample-config-advanced.json" ], "dependencies": { + "@yetzt/nedb": "^1.8.0", + "archiver": "^4.0.2", + "archiver-zip-encrypted": "^1.0.10", "body-parser": "^1.19.0", "cbor": "~5.2.0", "compression": "^1.7.4", @@ -43,13 +46,22 @@ "express": "^4.17.0", "express-handlebars": "^3.1.0", "express-ws": "^4.0.0", + "image-size": "^1.0.0", "ipcheck": "^0.1.0", + "loadavg-windows": "^1.1.1", "minimist": "^1.2.5", "multiparty": "^4.2.1", - "@yetzt/nedb": "^1.8.0", "node-forge": "^0.10.0", + "node-rdpjs-2": "^0.3.5", + "node-windows": "^0.1.4", + "otplib": "^10.2.3", + "pg": "^8.7.1", + "pgtools": "^0.3.2", + "ssh2": "^1.5.0", + "web-push": "^3.4.5", "ws": "^5.2.3", - "yauzl": "^2.10.0" + "yauzl": "^2.10.0", + "yubikeyotp": "^0.2.0" }, "engines": { "node": ">=10.0.0" diff --git a/webserver.js b/webserver.js index 3bf68ebf..8bb3bf34 100644 --- a/webserver.js +++ b/webserver.js @@ -84,6 +84,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF obj.blockedAgents = 0; obj.renderPages = null; obj.renderLanguages = []; + obj.destroyedSessions = {}; // Mesh Rights const MESHRIGHT_EDITMESH = 0x00000001; @@ -768,6 +769,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (req.session.userid) { var user = obj.users[req.session.userid]; if (user != null) { obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id }); } + if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session } req.session = null; parent.debug('web', 'handleLogoutRequest: success.'); @@ -1260,6 +1262,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF delete req.session.currentNode; req.session.userid = userid; req.session.ip = req.clientIp; + setSessionRandom(req); // If a login token was used, add this information and expire time to the session. if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { @@ -1423,6 +1426,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF obj.users[user._id] = user; req.session.userid = user._id; req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); // Create a user, generate a salt and hash the password require('./pass').hash(req.body.password1, function (err, salt, hash, tag) { if (err) throw err; @@ -1531,6 +1535,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF parent.debug('web', 'handleResetPasswordRequest: success'); req.session.userid = userid; req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tuser, req.session.tpass, direct, loginOptions); }, 0); } @@ -2425,6 +2430,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF parent.DispatchEvent(targets, obj, event); req.session.userid = userid; + setSessionRandom(req); } else { // New users not allowed parent.debug('web', 'handleStrategyLogin: Can\'t create new accounts'); @@ -2449,6 +2455,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid); req.session.userid = userid; + setSessionRandom(req); } } //res.redirect(domain.url); // This does not handle cookie correctly. @@ -2500,6 +2507,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF req.session.userid = userid; delete req.session.currentNode; req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); handleRootRequestEx(req, res, domain, direct); }); } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) { @@ -2531,6 +2539,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF req.session.userid = 'user/' + domain.id + '/~'; delete req.session.currentNode; req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); if (obj.users[req.session.userid] == null) { // Create the dummy user ~ with impossible password parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.'); @@ -2544,6 +2553,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); delete req.session.currentNode; req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) { var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout //if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // If the cookie if binded to an IP address, check here. @@ -2554,6 +2564,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF req.session.userid = loginCookie.u; delete req.session.currentNode; req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); } else { parent.debug('web', 'handleRootRequestEx: cookie auth failed.'); } @@ -2570,6 +2581,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF req.session.usersGroups = req.connection.userGroups; delete req.session.currentNode; req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); // Check if this user exists, create it if not. user = obj.users[req.session.userid]; @@ -5576,6 +5588,20 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Add HTTP security headers to all responses obj.app.use(function (req, res, next) { + // Check if a session is destroyed + if (typeof req.session.userid == 'string') { + if (typeof req.session.x == 'string') { + if (obj.destroyedSessions[req.session.userid + '/' + req.session.x] != null) { + delete req.session.userid; + delete req.session.ip; + delete req.session.t; + delete req.session.x; + } + } else { + // Legacy session without a random, add one. + setSessionRandom(req); + } + } // Remove legacy values from the session to keep the session as small as possible delete req.session.domainid; @@ -7932,5 +7958,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } } + // Set a random value to this session. Only works if the session has a userid. + // This random value along with the userid is used to destroy the session when logging out. + function setSessionRandom(req) { + if ((req.session == null) || (req.session.userid == null) || (req.session.x != null)) return; + var x = obj.crypto.randomBytes(6).toString('base64'); + while (obj.destroyedSessions[req.session.userid + '/' + x] != null) { x = obj.crypto.randomBytes(6).toString('base64'); } + req.session.x = x; + } + + // Remove all destroyed sessions after 2 hours, these sessions would have timed out anyway. + function clearDestroyedSessions() { + var toRemove = [], t = Date.now() - (2 * 60 * 60 * 1000); + for (var i in obj.destroyedSessions) { if (obj.destroyedSessions[i] < t) { toRemove.push(i); } } + for (var i in toRemove) { delete obj.destroyedSessions[toRemove[i]]; } + } + return obj; };