Browser session security improvements.

Ylian Saint-Hilaire committed Jul 12, 2022 at 17:45 UTC 66b03156241fef1754da5c1e740e429fe0d3fb88
2 files changed +70 -24
meshcentral.js
+23
@@ -3262,6 +3262,29 @@ function CreateMeshCentralServer(config, args) {
3262 });
3263 }
3264
3265 + // Encrypt session data
3266 + obj.encryptSessionData = function (data, key) {
3267 + if (data == null) return null;
3268 + if (key == null) { key = obj.loginCookieEncryptionKey; }
3269 + try {
3270 + const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
3271 + const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
3272 + return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
3273 + } catch (ex) { return null; }
3274 + }
3275 +
3276 + // Decrypt the session data
3277 + obj.decryptSessionData = function (data, key) {
3278 + if ((typeof data != 'string') || (data.length < 13)) return {};
3279 + if (key == null) { key = obj.loginCookieEncryptionKey; }
3280 + try {
3281 + const buf = Buffer.from(data, 'base64');
3282 + const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
3283 + decipher.setAuthTag(buf.slice(12, 28));
3284 + return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
3285 + } catch (ex) { return {}; }
3286 + }
3287 +
3288 // Generate a cryptographic key used to encode and decode cookies
3289 obj.generateCookieKey = function () {
3290 return Buffer.from(obj.crypto.randomBytes(80), 'binary');
webserver.js
+47 -24
@@ -925,8 +925,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
925 var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
926 if (httpport != 443) { origin += ':' + httpport; }
927
928 + const sec = parent.decryptSessionData(req.session.e);
929 var assertionExpectations = {
929 - challenge: req.session.u2f,
930 + challenge: sec.u2f,
931 origin: origin,
932 factor: 'either',
933 fmt: 'fido-u2f',
@@ -1006,6 +1007,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1007 // Return a U2F hardware key challenge
1008 function getHardwareKeyChallenge(req, domain, user, func) {
1009 delete req.session.u2f;
1010 + const sec = parent.decryptSessionData(req.session.e);
1011 +
1012 if (user.otphkeys && (user.otphkeys.length > 0)) {
1013 // Get all WebAuthn keys
1014 var webAuthnKeys = [];
@@ -1014,12 +1017,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1017 // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
1018 var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
1019 for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
1017 - req.session.u2f = authnOptions.challenge;
1020 + sec.u2f = authnOptions.challenge;
1021 + req.session.e = parent.encryptSessionData(sec);
1022 parent.debug('web', 'getHardwareKeyChallenge: success');
1023 func(JSON.stringify(authnOptions));
1024 return;
1025 }
1026 }
1027 +
1028 + // Remove the chalange if present
1029 + if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
1030 +
1031 parent.debug('web', 'getHardwareKeyChallenge: fail');
1032 func('');
1033 }
@@ -1049,7 +1057,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1057
1058 // Normally, use the body username/password. If this is a token, use the username/password in the session.
1059 var xusername = req.body.username, xpassword = req.body.password;
1052 - if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tuser; xpassword = req.session.tpass; }
1060 + if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
1061 + const sec = parent.decryptSessionData(req.session.e);
1062 + xusername = sec.tuser; xpassword = sec.tpass;
1063 + }
1064
1065 // Authenticate the user
1066 obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
@@ -1165,9 +1176,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1176 if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
1177 if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
1178 if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
1168 - req.session.tuserid = userid;
1169 - req.session.tuser = xusername;
1170 - req.session.tpass = xpassword;
1179 + req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
1180 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1181 }, randomWaitTime);
1182 } else {
@@ -1263,9 +1272,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1272 parent.debug('web', 'handleLoginRequest: login ok, password change requested');
1273 req.session.loginmode = 6;
1274 req.session.messageid = 113; // Password change requested.
1266 - req.session.resettokenuserid = userid;
1267 - req.session.resettokenusername = xusername;
1268 - req.session.resettokenpassword = xpassword;
1275 +
1276 + // Decrypt any session data
1277 + const sec = parent.decryptSessionData(req.session.e);
1278 + sec.rtuser = xusername;
1279 + sec.rtpass = xpassword;
1280 + req.session.e = parent.encryptSessionData(sec);
1281 +
1282 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1283 return;
1284 }
@@ -1289,6 +1302,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1302 // Regenerate session when signing in to prevent fixation
1303 //req.session.regenerate(function () {
1304 // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
1305 + delete req.session.e;
1306 delete req.session.u2f;
1307 delete req.session.loginmode;
1308 delete req.session.tuserid;
@@ -1513,18 +1527,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1527 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1528 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1529
1530 + // Decrypt any session data
1531 + const sec = parent.decryptSessionData(req.session.e);
1532 +
1533 // Check everything is ok
1534 const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
1518 - if ((allowAccountReset === false) || (domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.body.rpassword1 != 'string') || (typeof req.body.rpassword2 != 'string') || (req.body.rpassword1 != req.body.rpassword2) || (typeof req.body.rpasswordhint != 'string') || (req.session == null) || (typeof req.session.resettokenusername != 'string') || (typeof req.session.resettokenpassword != 'string')) {
1535 + if ((allowAccountReset === false) || (domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.body.rpassword1 != 'string') || (typeof req.body.rpassword2 != 'string') || (req.body.rpassword1 != req.body.rpassword2) || (typeof req.body.rpasswordhint != 'string') || (req.session == null) || (typeof sec.rtuser != 'string') || (typeof sec.rtpass != 'string')) {
1536 parent.debug('web', 'handleResetPasswordRequest: checks failed');
1537 + delete req.session.e;
1538 delete req.session.u2f;
1539 delete req.session.loginmode;
1540 delete req.session.tuserid;
1541 delete req.session.tuser;
1542 delete req.session.tpass;
1525 - delete req.session.resettokenuserid;
1526 - delete req.session.resettokenusername;
1527 - delete req.session.resettokenpassword;
1543 delete req.session.temail;
1544 delete req.session.tsms;
1545 delete req.session.tpush;
@@ -1536,7 +1551,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1551 }
1552
1553 // Authenticate the user
1539 - obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint, loginOptions) {
1554 + obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
1555 if (userid) {
1556 // Login
1557 var user = obj.users[userid];
@@ -1593,21 +1608,20 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1608 req.session.userid = userid;
1609 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1610 setSessionRandom(req);
1596 - completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tuser, req.session.tpass, direct, loginOptions);
1611 + const sec = parent.decryptSessionData(req.session.e);
1612 + completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
1613 }, 0);
1614 }
1615 }, 0);
1616 } else {
1617 // Failed, error out.
1618 parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
1619 + delete req.session.e;
1620 delete req.session.u2f;
1621 delete req.session.loginmode;
1622 delete req.session.tuserid;
1623 delete req.session.tuser;
1624 delete req.session.tpass;
1608 - delete req.session.resettokenuserid;
1609 - delete req.session.resettokenusername;
1610 - delete req.session.resettokenpassword;
1625 delete req.session.temail;
1626 delete req.session.tsms;
1627 delete req.session.tpush;
@@ -2819,6 +2833,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2833
2834 // Clean up the U2F challenge if needed
2835 if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
2836 + if (dbGetFunc.req.session.e) {
2837 + const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
2838 + if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
2839 + }
2840
2841 // Intel AMT Scanning options
2842 var amtscanoptions = '';
@@ -2894,8 +2912,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2912 // Send back the login application
2913 // If this is a 2 factor auth request, look for a hardware key challenge.
2914 // Normal login 2 factor request
2897 - if (req.session && (req.session.loginmode == 4) && (req.session.tuserid)) {
2898 - var user = obj.users[req.session.tuserid];
2915 + const sec = parent.decryptSessionData(req.session.e);
2916 + if (req.session && (req.session.loginmode == 4) && (sec.tuserid)) {
2917 + var user = obj.users[sec.tuserid];
2918 if (user != null) {
2919 parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
2920 getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
@@ -3027,7 +3046,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3046
3047 // Encrypt the hardware key challenge state if needed
3048 var hwstate = null;
3030 - if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tuser, p: req.session.tpass, c: req.session.u2f }, obj.parent.loginCookieEncryptionKey) }
3049 + if (hardwareKeyChallenge) {
3050 + const sec = parent.decryptSessionData(req.session.e);
3051 + hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
3052 + }
3053
3054 // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
3055 var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
@@ -3125,7 +3147,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3147 case 'tokenlogin': {
3148 if (req.body.hwstate) {
3149 var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
3128 - if (cookie != null) { req.session.tuser = cookie.u; req.session.tpass = cookie.p; req.session.u2f = cookie.c; }
3150 + if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
3151 }
3152 handleLoginRequest(req, res, true); break;
3153 }
@@ -5830,8 +5852,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5852 }
5853
5854 // Remove legacy values from the session to keep the session as small as possible
5855 + delete req.session.u2f;
5856 delete req.session.domainid;
5834 - delete req.session.u2fchallenge
5857 delete req.session.nowInMinutes;
5858 delete req.session.tokenuserid;
5859 delete req.session.tokenusername;
@@ -5920,7 +5942,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5942 const domain = req.xdomain = getDomain(req);
5943 parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
5944
5923 - // Skip the rest is this is an agent connection
5945 + // Skip the rest if this is an agent connection
5946 if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
5947
5948 // Setup security headers
@@ -5936,6 +5958,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5958 if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
5959 }
5960
5961 + // Finish setup security headers
5962 const headers = {
5963 'Referrer-Policy': 'no-referrer',
5964 'X-XSS-Protection': '1; mode=block',