Added user session destruction on logout for improved security.

Ylian Saint-Hilaire committed Jan 6, 2022 at 15:05 UTC 933b9af89940a528ec815bb050fde5ede799c39e
2 files changed +56 -2
package.json
+14 -2
@@ -36,6 +36,9 @@
36 "sample-config-advanced.json"
37 ],
38 "dependencies": {
39 + "@yetzt/nedb": "^1.8.0",
40 + "archiver": "^4.0.2",
41 + "archiver-zip-encrypted": "^1.0.10",
42 "body-parser": "^1.19.0",
43 "cbor": "~5.2.0",
44 "compression": "^1.7.4",
@@ -43,13 +46,22 @@
46 "express": "^4.17.0",
47 "express-handlebars": "^3.1.0",
48 "express-ws": "^4.0.0",
49 + "image-size": "^1.0.0",
50 "ipcheck": "^0.1.0",
51 + "loadavg-windows": "^1.1.1",
52 "minimist": "^1.2.5",
53 "multiparty": "^4.2.1",
49 - "@yetzt/nedb": "^1.8.0",
54 "node-forge": "^0.10.0",
55 + "node-rdpjs-2": "^0.3.5",
56 + "node-windows": "^0.1.4",
57 + "otplib": "^10.2.3",
58 + "pg": "^8.7.1",
59 + "pgtools": "^0.3.2",
60 + "ssh2": "^1.5.0",
61 + "web-push": "^3.4.5",
62 "ws": "^5.2.3",
52 - "yauzl": "^2.10.0"
63 + "yauzl": "^2.10.0",
64 + "yubikeyotp": "^0.2.0"
65 },
66 "engines": {
67 "node": ">=10.0.0"
webserver.js
+42
@@ -84,6 +84,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
84 obj.blockedAgents = 0;
85 obj.renderPages = null;
86 obj.renderLanguages = [];
87 + obj.destroyedSessions = {};
88
89 // Mesh Rights
90 const MESHRIGHT_EDITMESH = 0x00000001;
@@ -768,6 +769,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
769 if (req.session.userid) {
770 var user = obj.users[req.session.userid];
771 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 }); }
772 + if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
773 }
774 req.session = null;
775 parent.debug('web', 'handleLogoutRequest: success.');
@@ -1260,6 +1262,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1262 delete req.session.currentNode;
1263 req.session.userid = userid;
1264 req.session.ip = req.clientIp;
1265 + setSessionRandom(req);
1266
1267 // If a login token was used, add this information and expire time to the session.
1268 if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
@@ -1423,6 +1426,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1426 obj.users[user._id] = user;
1427 req.session.userid = user._id;
1428 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1429 + setSessionRandom(req);
1430 // Create a user, generate a salt and hash the password
1431 require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
1432 if (err) throw err;
@@ -1531,6 +1535,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1535 parent.debug('web', 'handleResetPasswordRequest: success');
1536 req.session.userid = userid;
1537 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1538 + setSessionRandom(req);
1539 completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tuser, req.session.tpass, direct, loginOptions);
1540 }, 0);
1541 }
@@ -2425,6 +2430,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2430 parent.DispatchEvent(targets, obj, event);
2431
2432 req.session.userid = userid;
2433 + setSessionRandom(req);
2434 } else {
2435 // New users not allowed
2436 parent.debug('web', 'handleStrategyLogin: Can\'t create new accounts');
@@ -2449,6 +2455,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2455 }
2456 parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid);
2457 req.session.userid = userid;
2458 + setSessionRandom(req);
2459 }
2460 }
2461 //res.redirect(domain.url); // This does not handle cookie correctly.
@@ -2500,6 +2507,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2507 req.session.userid = userid;
2508 delete req.session.currentNode;
2509 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2510 + setSessionRandom(req);
2511 handleRootRequestEx(req, res, domain, direct);
2512 });
2513 } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
@@ -2531,6 +2539,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2539 req.session.userid = 'user/' + domain.id + '/~';
2540 delete req.session.currentNode;
2541 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2542 + setSessionRandom(req);
2543 if (obj.users[req.session.userid] == null) {
2544 // Create the dummy user ~ with impossible password
2545 parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
@@ -2544,6 +2553,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2553 req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
2554 delete req.session.currentNode;
2555 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2556 + setSessionRandom(req);
2557 } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
2558 var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2559 //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
2564 req.session.userid = loginCookie.u;
2565 delete req.session.currentNode;
2566 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2567 + setSessionRandom(req);
2568 } else {
2569 parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
2570 }
@@ -2570,6 +2581,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2581 req.session.usersGroups = req.connection.userGroups;
2582 delete req.session.currentNode;
2583 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2584 + setSessionRandom(req);
2585
2586 // Check if this user exists, create it if not.
2587 user = obj.users[req.session.userid];
@@ -5576,6 +5588,20 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5588
5589 // Add HTTP security headers to all responses
5590 obj.app.use(function (req, res, next) {
5591 + // Check if a session is destroyed
5592 + if (typeof req.session.userid == 'string') {
5593 + if (typeof req.session.x == 'string') {
5594 + if (obj.destroyedSessions[req.session.userid + '/' + req.session.x] != null) {
5595 + delete req.session.userid;
5596 + delete req.session.ip;
5597 + delete req.session.t;
5598 + delete req.session.x;
5599 + }
5600 + } else {
5601 + // Legacy session without a random, add one.
5602 + setSessionRandom(req);
5603 + }
5604 + }
5605
5606 // Remove legacy values from the session to keep the session as small as possible
5607 delete req.session.domainid;
@@ -7932,5 +7958,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7958 }
7959 }
7960
7961 + // Set a random value to this session. Only works if the session has a userid.
7962 + // This random value along with the userid is used to destroy the session when logging out.
7963 + function setSessionRandom(req) {
7964 + if ((req.session == null) || (req.session.userid == null) || (req.session.x != null)) return;
7965 + var x = obj.crypto.randomBytes(6).toString('base64');
7966 + while (obj.destroyedSessions[req.session.userid + '/' + x] != null) { x = obj.crypto.randomBytes(6).toString('base64'); }
7967 + req.session.x = x;
7968 + }
7969 +
7970 + // Remove all destroyed sessions after 2 hours, these sessions would have timed out anyway.
7971 + function clearDestroyedSessions() {
7972 + var toRemove = [], t = Date.now() - (2 * 60 * 60 * 1000);
7973 + for (var i in obj.destroyedSessions) { if (obj.destroyedSessions[i] < t) { toRemove.push(i); } }
7974 + for (var i in toRemove) { delete obj.destroyedSessions[toRemove[i]]; }
7975 + }
7976 +
7977 return obj;
7978 };