CVE-2024-26135 (published 2024-02-20T20:15Z)

MeshCentral is a full computer management web site. Versions prior to 1.1.21 a cross-site websocket hijacking (CSWSH) vulnerability within the control.ashx endpoint. This component is the primary mechanism used within MeshCentral to perform administrative actions on the server. The vulnerability is exploitable when an attacker is able to convince a victim end-user to click on a malicious link to a page hosting an attacker-controlled site. The attacker can then originate a cross-site websocket connection using client-side JavaScript code to connect to `control.ashx` as the victim user within MeshCentral. Version 1.1.21 contains a patch for this issue.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/Ylianst/MeshCentral
commit_details: repo_url: https://github.com/Ylianst/MeshCentral
author: Ylian Saint-Hilaire
author_date: 2024-02-17 11:22:38-08:00
msg: Added option to check HTTP origin.
repository: repo_url: https://github.com/Ylianst/MeshCentral
repo_name: Ylianst/MeshCentral
description: A complete web-based remote monitoring and management web site. Once setup you can install agents and perform remote desktop session to devices on the local network or over the Internet.
repo_language: HTML
owner: Ylianst
file_changes: filename: meshcentral-config-schema.json
change_type: ModificationType.MODIFY
diff: @@ -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."

programming_language: JavaScript
filename: meshcentral.js
change_type: ModificationType.MODIFY
diff: @@ -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(',');

programming_language: JavaScript
filename: default.handlebars
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,4 @@
-<!DOCTYPE html>
+﻿<!DOCTYPE html>
 <html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
 <head>
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
@@ -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) {

programming_language: HTML
filename: webserver.js
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: JavaScript