diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 645d3213..fb332e1e 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -1134,6 +1134,15 @@ "type": "string" } }, + "allowedOrigin": { + "type": [ "array", "boolean" ], + "default": false, + "uniqueItems": true, + "description": "A list of allowed hostnames for HTTP request origin header. If false, a default list is created, if true, all hostnames are allowed.", + "items": { + "type": "string" + } + }, "welcomeText": { "type": "string", "description": "Text that will be shown on the login screen." diff --git a/meshcentral.js b/meshcentral.js index 025b9067..7b8c9a4d 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -1312,6 +1312,7 @@ function CreateMeshCentralServer(config, args) { if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(','); } } if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(','); } } if (typeof obj.config.domains[i].ignoreagenthashcheck == 'string') { if (obj.config.domains[i].ignoreagenthashcheck == '') { delete obj.config.domains[i].ignoreagenthashcheck; } else { obj.config.domains[i].ignoreagenthashcheck = obj.config.domains[i].ignoreagenthashcheck.split(','); } } + if (typeof obj.config.domains[i].allowedorigin == 'string') { if (obj.config.domains[i].allowedorigin == '') { delete obj.config.domains[i].allowedorigin; } else { obj.config.domains[i].allowedorigin = obj.config.domains[i].allowedorigin.split(','); } } if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) { if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') { obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(','); diff --git a/views/default.handlebars b/views/default.handlebars index 3504d543..308e9c62 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -1,4 +1,4 @@ - + @@ -2133,6 +2133,7 @@ QV('verifyEmailId2', false); QV('logoutControl', false); if (errorCode == 'noauth') { QH('p0span', "Unable to perform authentication"); return; } + if (errorCode == 'invalidorigin') { QH('p0span', "Invalid origin in HTTP request"); return; } if (prevState == 2) { if (autoReconnect) { setTimeout(serverPoll, 5000); } } else { QH('p0span', "Unable to connect web socket"); } if (authCookieRenewTimer != null) { clearInterval(authCookieRenewTimer); authCookieRenewTimer = null; } } else if (state == 2) { diff --git a/webserver.js b/webserver.js index 4f574134..3f1d8df8 100644 --- a/webserver.js +++ b/webserver.js @@ -5734,6 +5734,18 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF return obj.certificates.CommonName; } + // Return true if this is an allowed HTTP request origin hostname. + obj.CheckWebServerOriginName = function (domain, req) { + if (domain.allowedorigin === true) return true; // Ignore origin + if (typeof req.headers.origin != 'string') return true; // No origin in the header, this is a desktop app + const originUrl = require('url').parse(req.headers.origin, true); + if (typeof originUrl.hostname != 'string') return false; // Origin hostname is not valid + if (Array.isArray(domain.allowedorigin)) return (domain.allowedorigin.indexOf(originUrl.hostname) >= 0); // Check if this is an allowed origin from an explicit list + if (obj.isTrustedCert(domain) === false) return true; // This server does not have a trusted certificate. + if (domain.dns != null) return (domain.dns == originUrl.hostname); // Match the domain DNS + return (obj.certificates.CommonName == originUrl.hostname); // Match the default server name + } + // Create a OSX mesh agent installer obj.handleMeshOsxAgentRequest = function (req, res) { const domain = getDomain(req, res); @@ -6434,6 +6446,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF obj.app.ws(url + 'control.ashx', function (ws, req) { getWebsocketArgs(ws, req, function (ws, req) { const domain = getDomain(req); + if (obj.CheckWebServerOriginName(domain, req) == false) { + try { ws.send(JSON.stringify({ action: 'close', cause: 'invalidorigin', msg: 'invalidorigin' })); } catch (ex) { } + try { ws.close(); } catch (ex) { } + return; + } if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { ws.close(); return; } // Check 3FA URL key PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) { if (user == null) { // User is not authenticated, perform inner server authentication