diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index b226c32e..80825fb5 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -63,7 +63,7 @@ "certificatePrivateKeyPassword": { "type": "array", "default": null, "description": "List of passwords used to decrypt PKCK#8 .key files that are in the meshcentral-data folder." }, "sessionTime": { "type": "integer", "default": 60, "description": "Duration of a session cookie in minutes. Changing this affects how often the session needs to be automatically refreshed." }, "sessionKey": { "type": "string", "default": null, "description": "Password used to encrypt the MeshCentral web session cookies. If null, a random one is generated each time the server starts." }, - "sessionSameSite": { "type": "string", "default": "strict", "enum": ["strict", "lax", "none"] }, + "cookieSameSite": { "type": "string", "default": "lax", "enum": ["strict", "lax", "none"] }, "dbEncryptKey": { "type": "string" }, "dbRecordsEncryptKey": { "type": "string", "default": null }, "dbRecordsDecryptKey": { "type": "string", "default": null }, @@ -94,7 +94,7 @@ "allowLoginToken": { "type": "boolean", "default": false }, "StrictTransportSecurity": { "type": ["boolean", "string"], "default": null, "description": "Controls the Strict-Transport-Security header, default is 1 year. Set to false to remove, true to force enable, or string to set a custom value. If set to null, MeshCentral will enable if a trusted certificate is set." }, "allowFraming": { "type": "boolean", "default": false, "description": "When enabled, the MeshCentral web site can be embedded within another website's iframe." }, - "cookieIpCheck": { "type": "boolean" }, + "cookieIpCheck": { "type": [ "string", "boolean" ], "default": "lax", "enum": ["strict", "lax", "none"] }, "cookieEncoding": { "type": "string", "enum": [ "hex", "base64" ], "default": "base64", "description": "Encoding format of cookies in the HTTP headers, this is typically Base64 but some reverse proxies will require HEX." }, "webRTC": { "type": "boolean", "default": false, "description": "When enabled, allows use of WebRTC to allow direct network traffic between the agent and browser." }, "nice404": { "type": "boolean", "default": true, "description": "By default, a nice looking 404 error page is displayed when needed. Set this to false to disable it." }, diff --git a/meshcentral.js b/meshcentral.js index 2c5179b6..874130a5 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -775,6 +775,15 @@ function CreateMeshCentralServer(config, args) { if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); } if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); } + // Check the "cookieIpCheck" value + if (obj.args.cookieipcheck === false) { obj.args.cookieipcheck = 'none'; } + else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; } + else { obj.args.cookieipcheck = 'strict'; } + + // Check the "cookieSameSite" value + if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; } + else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); } + // Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2 const verSplit = process.version.substring(1).split('.'); var ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100); diff --git a/webserver.js b/webserver.js index 8c4393c8..e0a4b14c 100644 --- a/webserver.js +++ b/webserver.js @@ -811,7 +811,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF for (var i in cookies) { if (cookies[i].startsWith('twofactor=')) { var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire feild, assume 30 day timeout. - if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; } + if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; } } } } @@ -835,7 +835,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF for (var i in cookies) { if (cookies[i].startsWith('twofactor=')) { var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire feild, assume 30 day timeout. - if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; } + if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; } } } } @@ -862,7 +862,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Check 2FA login cookie if ((token != null) && (token.startsWith('cookie='))) { var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(token.substring(7)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire feild, assume 30 day timeout. - if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; } + if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; } } // Check email key @@ -1168,7 +1168,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var maxCookieAge = domain.twofactorcookiedurationdays; if (typeof maxCookieAge != 'number') { maxCookieAge = 30; } const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey); - res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'), secure: true }); + res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true }); } // Check if email address needs to be confirmed @@ -2625,7 +2625,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF 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. + //if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie if binded to an IP address, check here. if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) { // If a login cookie was provided, setup the session here. parent.debug('web', 'handleRootRequestEx: cookie auth ok.'); @@ -3087,7 +3087,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var maxCookieAge = domain.twofactorcookiedurationdays; if (typeof maxCookieAge != 'number') { maxCookieAge = 30; } const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey); - res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'), secure: true }); + res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true }); } handleRootRequestEx(req, res, domain); @@ -3853,7 +3853,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // If an authentication cookie is embedded in the form, use that. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding. + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication } if (authUserid == null) { res.sendStatus(401); return; } @@ -3896,7 +3896,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // If an authentication cookie is embedded in the form, use that. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding. + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication } if (authUserid == null) { res.sendStatus(401); return; } @@ -3936,7 +3936,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // If an authentication cookie is embedded in the form, use that. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding. + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication } if (authUserid == null) { res.sendStatus(401); return; } @@ -4036,7 +4036,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // If an authentication cookie is embedded in the form, use that. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding. + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication } if (authUserid == null) { res.sendStatus(401); return; } @@ -4859,7 +4859,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // If an authentication cookie is embedded in the form, use that. if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding. + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication } if (authUserid == null) { res.sendStatus(401); return; } @@ -5700,9 +5700,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF name: 'xid', // Recommended security practice to not use the default cookie name httpOnly: true, keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances - secure: (obj.args.tlsoffload == null) // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html) + secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html) + sameSite: obj.args.cookiesamesite } - if (obj.args.sessionsamesite != null) { sessionOptions.sameSite = obj.args.sessionsamesite; } else { sessionOptions.sameSite = ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'); } if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60 * 1000); } obj.app.use(obj.session(sessionOptions)); @@ -5834,7 +5834,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF res.set(headers); // Check the session if bound to the external IP address - if ((parent.config.settings.cookieipcheck !== false) && (req.session.ip != null) && (req.clientIp != null) && (req.session.ip != req.clientIp)) { req.session = {}; } + if ((req.session.ip != null) && (req.clientIp != null) && checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; } // Extend the session time by forcing a change to the session every minute. if (req.session.userid != null) { req.session.t = Math.floor(Date.now() / 60e3); } else { delete req.session.t; } @@ -6817,7 +6817,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // This is a encrypted cookie authentication var cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout if ((cookie == null) && (obj.parent.multiServer != null)) { cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.serverKey, 60); } // Try the server key - if ((obj.args.cookieipcheck !== false) && (cookie != null) && (cookie.ip != null) && (cookie.ip != req.clientIp && (cookie.ip != req.clientIp))) { // If the cookie if binded to an IP address, check here. + if ((cookie != null) && (cookie.ip != null) && checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here. parent.debug('web', 'ERR: Invalid cookie IP address, got \"' + cookie.ip + '\", expected \"' + cleanRemoteAddr(req.clientIp) + '\".'); cookie = null; } @@ -8201,5 +8201,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF return header + value + '\r\n'; } + // Check that a cookie IP is within the correct range depending on the active policy + function checkCookieIp(cookieip, ip) { + if (obj.args.cookieipcheck == 'none') return true; // 'none' - No IP address checking + if (obj.args.cookieipcheck == 'strict') return (cookieip == ip); // 'strict' - Strict IP address checking, this can cause issues with HTTP proxies or load-balancers. + return require('ipcheck').match(cookieip, ip + '/24'); // 'lax' - IP address need to be in the some range + } + return obj; };