mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2024-11-23 06:34:54 +03:00
Browser session security improvements.
This commit is contained in:
parent
ef41a18269
commit
66b0315624
@ -3262,6 +3262,29 @@ function CreateMeshCentralServer(config, args) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypt session data
|
||||||
|
obj.encryptSessionData = function (data, key) {
|
||||||
|
if (data == null) return null;
|
||||||
|
if (key == null) { key = obj.loginCookieEncryptionKey; }
|
||||||
|
try {
|
||||||
|
const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
|
||||||
|
const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
|
||||||
|
return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
|
||||||
|
} catch (ex) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the session data
|
||||||
|
obj.decryptSessionData = function (data, key) {
|
||||||
|
if ((typeof data != 'string') || (data.length < 13)) return {};
|
||||||
|
if (key == null) { key = obj.loginCookieEncryptionKey; }
|
||||||
|
try {
|
||||||
|
const buf = Buffer.from(data, 'base64');
|
||||||
|
const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
|
||||||
|
decipher.setAuthTag(buf.slice(12, 28));
|
||||||
|
return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
|
||||||
|
} catch (ex) { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a cryptographic key used to encode and decode cookies
|
// Generate a cryptographic key used to encode and decode cookies
|
||||||
obj.generateCookieKey = function () {
|
obj.generateCookieKey = function () {
|
||||||
return Buffer.from(obj.crypto.randomBytes(80), 'binary');
|
return Buffer.from(obj.crypto.randomBytes(80), 'binary');
|
||||||
|
71
webserver.js
71
webserver.js
@ -925,8 +925,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
|
var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
|
||||||
if (httpport != 443) { origin += ':' + httpport; }
|
if (httpport != 443) { origin += ':' + httpport; }
|
||||||
|
|
||||||
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
var assertionExpectations = {
|
var assertionExpectations = {
|
||||||
challenge: req.session.u2f,
|
challenge: sec.u2f,
|
||||||
origin: origin,
|
origin: origin,
|
||||||
factor: 'either',
|
factor: 'either',
|
||||||
fmt: 'fido-u2f',
|
fmt: 'fido-u2f',
|
||||||
@ -1006,6 +1007,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
// Return a U2F hardware key challenge
|
// Return a U2F hardware key challenge
|
||||||
function getHardwareKeyChallenge(req, domain, user, func) {
|
function getHardwareKeyChallenge(req, domain, user, func) {
|
||||||
delete req.session.u2f;
|
delete req.session.u2f;
|
||||||
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
|
|
||||||
if (user.otphkeys && (user.otphkeys.length > 0)) {
|
if (user.otphkeys && (user.otphkeys.length > 0)) {
|
||||||
// Get all WebAuthn keys
|
// Get all WebAuthn keys
|
||||||
var webAuthnKeys = [];
|
var webAuthnKeys = [];
|
||||||
@ -1014,12 +1017,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
// Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
|
// Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
|
||||||
var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
|
var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
|
||||||
for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
|
for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
|
||||||
req.session.u2f = authnOptions.challenge;
|
sec.u2f = authnOptions.challenge;
|
||||||
|
req.session.e = parent.encryptSessionData(sec);
|
||||||
parent.debug('web', 'getHardwareKeyChallenge: success');
|
parent.debug('web', 'getHardwareKeyChallenge: success');
|
||||||
func(JSON.stringify(authnOptions));
|
func(JSON.stringify(authnOptions));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the chalange if present
|
||||||
|
if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
|
||||||
|
|
||||||
parent.debug('web', 'getHardwareKeyChallenge: fail');
|
parent.debug('web', 'getHardwareKeyChallenge: fail');
|
||||||
func('');
|
func('');
|
||||||
}
|
}
|
||||||
@ -1049,7 +1057,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
|
|
||||||
// Normally, use the body username/password. If this is a token, use the username/password in the session.
|
// Normally, use the body username/password. If this is a token, use the username/password in the session.
|
||||||
var xusername = req.body.username, xpassword = req.body.password;
|
var xusername = req.body.username, xpassword = req.body.password;
|
||||||
if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tuser; xpassword = req.session.tpass; }
|
if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
|
||||||
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
|
xusername = sec.tuser; xpassword = sec.tpass;
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate the user
|
// Authenticate the user
|
||||||
obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
|
obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
|
||||||
@ -1165,9 +1176,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
|
if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
|
||||||
if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
|
if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
|
||||||
if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
|
if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
|
||||||
req.session.tuserid = userid;
|
req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
|
||||||
req.session.tuser = xusername;
|
|
||||||
req.session.tpass = xpassword;
|
|
||||||
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
|
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
|
||||||
}, randomWaitTime);
|
}, randomWaitTime);
|
||||||
} else {
|
} else {
|
||||||
@ -1263,9 +1272,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
parent.debug('web', 'handleLoginRequest: login ok, password change requested');
|
parent.debug('web', 'handleLoginRequest: login ok, password change requested');
|
||||||
req.session.loginmode = 6;
|
req.session.loginmode = 6;
|
||||||
req.session.messageid = 113; // Password change requested.
|
req.session.messageid = 113; // Password change requested.
|
||||||
req.session.resettokenuserid = userid;
|
|
||||||
req.session.resettokenusername = xusername;
|
// Decrypt any session data
|
||||||
req.session.resettokenpassword = xpassword;
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
|
sec.rtuser = xusername;
|
||||||
|
sec.rtpass = xpassword;
|
||||||
|
req.session.e = parent.encryptSessionData(sec);
|
||||||
|
|
||||||
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
|
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1289,6 +1302,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
// Regenerate session when signing in to prevent fixation
|
// Regenerate session when signing in to prevent fixation
|
||||||
//req.session.regenerate(function () {
|
//req.session.regenerate(function () {
|
||||||
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
|
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
|
||||||
|
delete req.session.e;
|
||||||
delete req.session.u2f;
|
delete req.session.u2f;
|
||||||
delete req.session.loginmode;
|
delete req.session.loginmode;
|
||||||
delete req.session.tuserid;
|
delete req.session.tuserid;
|
||||||
@ -1513,18 +1527,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
|
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
|
||||||
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
|
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
|
||||||
|
|
||||||
|
// Decrypt any session data
|
||||||
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
|
|
||||||
// Check everything is ok
|
// Check everything is ok
|
||||||
const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
|
const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
|
||||||
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')) {
|
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')) {
|
||||||
parent.debug('web', 'handleResetPasswordRequest: checks failed');
|
parent.debug('web', 'handleResetPasswordRequest: checks failed');
|
||||||
|
delete req.session.e;
|
||||||
delete req.session.u2f;
|
delete req.session.u2f;
|
||||||
delete req.session.loginmode;
|
delete req.session.loginmode;
|
||||||
delete req.session.tuserid;
|
delete req.session.tuserid;
|
||||||
delete req.session.tuser;
|
delete req.session.tuser;
|
||||||
delete req.session.tpass;
|
delete req.session.tpass;
|
||||||
delete req.session.resettokenuserid;
|
|
||||||
delete req.session.resettokenusername;
|
|
||||||
delete req.session.resettokenpassword;
|
|
||||||
delete req.session.temail;
|
delete req.session.temail;
|
||||||
delete req.session.tsms;
|
delete req.session.tsms;
|
||||||
delete req.session.tpush;
|
delete req.session.tpush;
|
||||||
@ -1536,7 +1551,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate the user
|
// Authenticate the user
|
||||||
obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint, loginOptions) {
|
obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
|
||||||
if (userid) {
|
if (userid) {
|
||||||
// Login
|
// Login
|
||||||
var user = obj.users[userid];
|
var user = obj.users[userid];
|
||||||
@ -1593,21 +1608,20 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
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);
|
setSessionRandom(req);
|
||||||
completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tuser, req.session.tpass, direct, loginOptions);
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
|
completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
// Failed, error out.
|
// Failed, error out.
|
||||||
parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
|
parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
|
||||||
|
delete req.session.e;
|
||||||
delete req.session.u2f;
|
delete req.session.u2f;
|
||||||
delete req.session.loginmode;
|
delete req.session.loginmode;
|
||||||
delete req.session.tuserid;
|
delete req.session.tuserid;
|
||||||
delete req.session.tuser;
|
delete req.session.tuser;
|
||||||
delete req.session.tpass;
|
delete req.session.tpass;
|
||||||
delete req.session.resettokenuserid;
|
|
||||||
delete req.session.resettokenusername;
|
|
||||||
delete req.session.resettokenpassword;
|
|
||||||
delete req.session.temail;
|
delete req.session.temail;
|
||||||
delete req.session.tsms;
|
delete req.session.tsms;
|
||||||
delete req.session.tpush;
|
delete req.session.tpush;
|
||||||
@ -2819,6 +2833,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
|
|
||||||
// Clean up the U2F challenge if needed
|
// Clean up the U2F challenge if needed
|
||||||
if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
|
if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
|
||||||
|
if (dbGetFunc.req.session.e) {
|
||||||
|
const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
|
||||||
|
if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
|
||||||
|
}
|
||||||
|
|
||||||
// Intel AMT Scanning options
|
// Intel AMT Scanning options
|
||||||
var amtscanoptions = '';
|
var amtscanoptions = '';
|
||||||
@ -2894,8 +2912,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
// Send back the login application
|
// Send back the login application
|
||||||
// If this is a 2 factor auth request, look for a hardware key challenge.
|
// If this is a 2 factor auth request, look for a hardware key challenge.
|
||||||
// Normal login 2 factor request
|
// Normal login 2 factor request
|
||||||
if (req.session && (req.session.loginmode == 4) && (req.session.tuserid)) {
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
var user = obj.users[req.session.tuserid];
|
if (req.session && (req.session.loginmode == 4) && (sec.tuserid)) {
|
||||||
|
var user = obj.users[sec.tuserid];
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
|
parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
|
||||||
getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
|
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
|
|||||||
|
|
||||||
// Encrypt the hardware key challenge state if needed
|
// Encrypt the hardware key challenge state if needed
|
||||||
var hwstate = null;
|
var hwstate = null;
|
||||||
if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tuser, p: req.session.tpass, c: req.session.u2f }, obj.parent.loginCookieEncryptionKey) }
|
if (hardwareKeyChallenge) {
|
||||||
|
const sec = parent.decryptSessionData(req.session.e);
|
||||||
|
hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
|
// Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
|
||||||
var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
|
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
|
|||||||
case 'tokenlogin': {
|
case 'tokenlogin': {
|
||||||
if (req.body.hwstate) {
|
if (req.body.hwstate) {
|
||||||
var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
|
var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
|
||||||
if (cookie != null) { req.session.tuser = cookie.u; req.session.tpass = cookie.p; req.session.u2f = cookie.c; }
|
if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
|
||||||
}
|
}
|
||||||
handleLoginRequest(req, res, true); break;
|
handleLoginRequest(req, res, true); break;
|
||||||
}
|
}
|
||||||
@ -5830,8 +5852,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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.u2f;
|
||||||
delete req.session.domainid;
|
delete req.session.domainid;
|
||||||
delete req.session.u2fchallenge
|
|
||||||
delete req.session.nowInMinutes;
|
delete req.session.nowInMinutes;
|
||||||
delete req.session.tokenuserid;
|
delete req.session.tokenuserid;
|
||||||
delete req.session.tokenusername;
|
delete req.session.tokenusername;
|
||||||
@ -5920,7 +5942,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
const domain = req.xdomain = getDomain(req);
|
const domain = req.xdomain = getDomain(req);
|
||||||
parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
|
parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
|
||||||
|
|
||||||
// Skip the rest is this is an agent connection
|
// Skip the rest if this is an agent connection
|
||||||
if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
|
if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
|
||||||
|
|
||||||
// Setup security headers
|
// Setup security headers
|
||||||
@ -5936,6 +5958,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
|
|||||||
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
|
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finish setup security headers
|
||||||
const headers = {
|
const headers = {
|
||||||
'Referrer-Policy': 'no-referrer',
|
'Referrer-Policy': 'no-referrer',
|
||||||
'X-XSS-Protection': '1; mode=block',
|
'X-XSS-Protection': '1; mode=block',
|
||||||
|
Loading…
Reference in New Issue
Block a user