Added user session destruction on logout for improved security.

This commit is contained in:
Ylian Saint-Hilaire 2022-01-06 15:05:45 -08:00
parent b939c4b89e
commit 933b9af899
2 changed files with 56 additions and 2 deletions

View File

@ -36,6 +36,9 @@
"sample-config-advanced.json" "sample-config-advanced.json"
], ],
"dependencies": { "dependencies": {
"@yetzt/nedb": "^1.8.0",
"archiver": "^4.0.2",
"archiver-zip-encrypted": "^1.0.10",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"cbor": "~5.2.0", "cbor": "~5.2.0",
"compression": "^1.7.4", "compression": "^1.7.4",
@ -43,13 +46,22 @@
"express": "^4.17.0", "express": "^4.17.0",
"express-handlebars": "^3.1.0", "express-handlebars": "^3.1.0",
"express-ws": "^4.0.0", "express-ws": "^4.0.0",
"image-size": "^1.0.0",
"ipcheck": "^0.1.0", "ipcheck": "^0.1.0",
"loadavg-windows": "^1.1.1",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"multiparty": "^4.2.1", "multiparty": "^4.2.1",
"@yetzt/nedb": "^1.8.0",
"node-forge": "^0.10.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", "ws": "^5.2.3",
"yauzl": "^2.10.0" "yauzl": "^2.10.0",
"yubikeyotp": "^0.2.0"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@ -84,6 +84,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.blockedAgents = 0; obj.blockedAgents = 0;
obj.renderPages = null; obj.renderPages = null;
obj.renderLanguages = []; obj.renderLanguages = [];
obj.destroyedSessions = {};
// Mesh Rights // Mesh Rights
const MESHRIGHT_EDITMESH = 0x00000001; const MESHRIGHT_EDITMESH = 0x00000001;
@ -768,6 +769,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
if (req.session.userid) { if (req.session.userid) {
var user = obj.users[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 (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; req.session = null;
parent.debug('web', 'handleLogoutRequest: success.'); parent.debug('web', 'handleLogoutRequest: success.');
@ -1260,6 +1262,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
delete req.session.currentNode; delete req.session.currentNode;
req.session.userid = userid; req.session.userid = userid;
req.session.ip = req.clientIp; req.session.ip = req.clientIp;
setSessionRandom(req);
// If a login token was used, add this information and expire time to the session. // If a login token was used, add this information and expire time to the session.
if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { 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; obj.users[user._id] = user;
req.session.userid = user._id; req.session.userid = user._id;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request 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 // Create a user, generate a salt and hash the password
require('./pass').hash(req.body.password1, function (err, salt, hash, tag) { require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
if (err) throw err; if (err) throw err;
@ -1531,6 +1535,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
parent.debug('web', 'handleResetPasswordRequest: success'); parent.debug('web', 'handleResetPasswordRequest: success');
req.session.userid = userid; req.session.userid = userid;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request 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); completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tuser, req.session.tpass, direct, loginOptions);
}, 0); }, 0);
} }
@ -2425,6 +2430,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
parent.DispatchEvent(targets, obj, event); parent.DispatchEvent(targets, obj, event);
req.session.userid = userid; req.session.userid = userid;
setSessionRandom(req);
} else { } else {
// New users not allowed // New users not allowed
parent.debug('web', 'handleStrategyLogin: Can\'t create new accounts'); 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); parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid);
req.session.userid = userid; req.session.userid = userid;
setSessionRandom(req);
} }
} }
//res.redirect(domain.url); // This does not handle cookie correctly. //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; req.session.userid = userid;
delete req.session.currentNode; delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
handleRootRequestEx(req, res, domain, direct); handleRootRequestEx(req, res, domain, direct);
}); });
} else if ((req.session != null) && (typeof req.session.loginToken == 'string')) { } 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 + '/~'; req.session.userid = 'user/' + domain.id + '/~';
delete req.session.currentNode; delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
if (obj.users[req.session.userid] == null) { if (obj.users[req.session.userid] == null) {
// Create the dummy user ~ with impossible password // Create the dummy user ~ with impossible password
parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.'); 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(); req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
delete req.session.currentNode; delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request 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)) { } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout 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. //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; req.session.userid = loginCookie.u;
delete req.session.currentNode; delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
} else { } else {
parent.debug('web', 'handleRootRequestEx: cookie auth failed.'); 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; req.session.usersGroups = req.connection.userGroups;
delete req.session.currentNode; delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request 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. // Check if this user exists, create it if not.
user = obj.users[req.session.userid]; 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 // Add HTTP security headers to all responses
obj.app.use(function (req, res, next) { 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 // Remove legacy values from the session to keep the session as small as possible
delete req.session.domainid; 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; return obj;
}; };