CookieIpCheck now has none/lax/strict options, with default being lax. #3861

Ylian Saint-Hilaire committed Apr 9, 2022 at 17:12 UTC 8fce45ad767ec291483de4838f79d71ee2fa024b
3 files changed +33 -17
meshcentral-config-schema.json
+2 -2
@@ -63,7 +63,7 @@
63 "certificatePrivateKeyPassword": { "type": "array", "default": null, "description": "List of passwords used to decrypt PKCK#8 .key files that are in the meshcentral-data folder." },
64 "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." },
65 "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." },
66 - "sessionSameSite": { "type": "string", "default": "strict", "enum": ["strict", "lax", "none"] },
66 + "cookieSameSite": { "type": "string", "default": "lax", "enum": ["strict", "lax", "none"] },
67 "dbEncryptKey": { "type": "string" },
68 "dbRecordsEncryptKey": { "type": "string", "default": null },
69 "dbRecordsDecryptKey": { "type": "string", "default": null },
@@ -94,7 +94,7 @@
94 "allowLoginToken": { "type": "boolean", "default": false },
95 "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." },
96 "allowFraming": { "type": "boolean", "default": false, "description": "When enabled, the MeshCentral web site can be embedded within another website's iframe." },
97 - "cookieIpCheck": { "type": "boolean" },
97 + "cookieIpCheck": { "type": [ "string", "boolean" ], "default": "lax", "enum": ["strict", "lax", "none"] },
98 "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." },
99 "webRTC": { "type": "boolean", "default": false, "description": "When enabled, allows use of WebRTC to allow direct network traffic between the agent and browser." },
100 "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." },
meshcentral.js
+9
@@ -775,6 +775,15 @@ function CreateMeshCentralServer(config, args) {
775 if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); }
776 if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); }
777
778 + // Check the "cookieIpCheck" value
779 + if (obj.args.cookieipcheck === false) { obj.args.cookieipcheck = 'none'; }
780 + else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; }
781 + else { obj.args.cookieipcheck = 'strict'; }
782 +
783 + // Check the "cookieSameSite" value
784 + if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; }
785 + else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); }
786 +
787 // Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2
788 const verSplit = process.version.substring(1).split('.');
789 var ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
webserver.js
+22 -15
@@ -811,7 +811,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
811 for (var i in cookies) {
812 if (cookies[i].startsWith('twofactor=')) {
813 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.
814 - if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
814 + if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
815 }
816 }
817 }
@@ -835,7 +835,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
835 for (var i in cookies) {
836 if (cookies[i].startsWith('twofactor=')) {
837 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.
838 - if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
838 + if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
839 }
840 }
841 }
@@ -862,7 +862,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
862 // Check 2FA login cookie
863 if ((token != null) && (token.startsWith('cookie='))) {
864 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.
865 - if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
865 + if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
866 }
867
868 // Check email key
@@ -1168,7 +1168,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1168 var maxCookieAge = domain.twofactorcookiedurationdays;
1169 if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
1170 const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
1171 - res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'), secure: true });
1171 + res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true });
1172 }
1173
1174 // Check if email address needs to be confirmed
@@ -2625,7 +2625,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2625 setSessionRandom(req);
2626 } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
2627 var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2628 - //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.
2628 + //if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie if binded to an IP address, check here.
2629 if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
2630 // If a login cookie was provided, setup the session here.
2631 parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
@@ -3087,7 +3087,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3087 var maxCookieAge = domain.twofactorcookiedurationdays;
3088 if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
3089 const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
3090 - res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'), secure: true });
3090 + res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true });
3091 }
3092
3093 handleRootRequestEx(req, res, domain);
@@ -3853,7 +3853,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3853 // If an authentication cookie is embedded in the form, use that.
3854 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3855 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3856 - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3856 + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3857 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3858 }
3859 if (authUserid == null) { res.sendStatus(401); return; }
@@ -3896,7 +3896,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3896 // If an authentication cookie is embedded in the form, use that.
3897 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3898 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3899 - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3899 + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3900 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3901 }
3902 if (authUserid == null) { res.sendStatus(401); return; }
@@ -3936,7 +3936,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3936 // If an authentication cookie is embedded in the form, use that.
3937 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3938 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3939 - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3939 + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3940 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3941 }
3942 if (authUserid == null) { res.sendStatus(401); return; }
@@ -4036,7 +4036,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
4036 // If an authentication cookie is embedded in the form, use that.
4037 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4038 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4039 - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4039 + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4040 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4041 }
4042 if (authUserid == null) { res.sendStatus(401); return; }
@@ -4859,7 +4859,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
4859 // If an authentication cookie is embedded in the form, use that.
4860 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4861 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4862 - if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4862 + if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4863 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4864 }
4865 if (authUserid == null) { res.sendStatus(401); return; }
@@ -5700,9 +5700,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5700 name: 'xid', // Recommended security practice to not use the default cookie name
5701 httpOnly: true,
5702 keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
5703 - secure: (obj.args.tlsoffload == null) // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
5703 + secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
5704 + sameSite: obj.args.cookiesamesite
5705 }
5705 - if (obj.args.sessionsamesite != null) { sessionOptions.sameSite = obj.args.sessionsamesite; } else { sessionOptions.sameSite = ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'); }
5706 if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60 * 1000); }
5707 obj.app.use(obj.session(sessionOptions));
5708
@@ -5834,7 +5834,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5834 res.set(headers);
5835
5836 // Check the session if bound to the external IP address
5837 - if ((parent.config.settings.cookieipcheck !== false) && (req.session.ip != null) && (req.clientIp != null) && (req.session.ip != req.clientIp)) { req.session = {}; }
5837 + if ((req.session.ip != null) && (req.clientIp != null) && checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; }
5838
5839 // Extend the session time by forcing a change to the session every minute.
5840 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
6817 // This is a encrypted cookie authentication
6818 var cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
6819 if ((cookie == null) && (obj.parent.multiServer != null)) { cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.serverKey, 60); } // Try the server key
6820 - 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.
6820 + if ((cookie != null) && (cookie.ip != null) && checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here.
6821 parent.debug('web', 'ERR: Invalid cookie IP address, got \"' + cookie.ip + '\", expected \"' + cleanRemoteAddr(req.clientIp) + '\".');
6822 cookie = null;
6823 }
@@ -8201,5 +8201,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8201 return header + value + '\r\n';
8202 }
8203
8204 + // Check that a cookie IP is within the correct range depending on the active policy
8205 + function checkCookieIp(cookieip, ip) {
8206 + if (obj.args.cookieipcheck == 'none') return true; // 'none' - No IP address checking
8207 + if (obj.args.cookieipcheck == 'strict') return (cookieip == ip); // 'strict' - Strict IP address checking, this can cause issues with HTTP proxies or load-balancers.
8208 + return require('ipcheck').match(cookieip, ip + '/24'); // 'lax' - IP address need to be in the some range
8209 + }
8210 +
8211 return obj;
8212 };