diff --git a/agents/meshinstall-linux.js b/agents/meshinstall-linux.js index 1f047afff0..1f64a2c26b 100644 --- a/agents/meshinstall-linux.js +++ b/agents/meshinstall-linux.js @@ -229,7 +229,13 @@ if ((!skip) && ((msh.InstallFlags & 2) == 2)) { if (process.argv[i].startsWith('--installPath=')) { - p.push('--installPath="' + process.argv[i].split('=').pop() + '"'); + var installPathValue = process.argv[i].split('=').pop(); + if (installPathValue.indexOf('..') != -1) + { + console.log('Invalid --installPath value: path traversal ("..") is not allowed.'); + process.exit(); + } + p.push('--installPath="' + installPathValue + '"'); } else if(process.argv[i].startsWith('--')) { @@ -359,3 +365,4 @@ if (!skip) process.exit(); }); } + diff --git a/plugins/migrate.js b/plugins/migrate.js index d59c4e364d..c2c76ebd1c 100644 --- a/plugins/migrate.js +++ b/plugins/migrate.js @@ -132,7 +132,8 @@ function loadConfig(configfile) { var raw = fs.readFileSync(configfile, 'utf8'); // Strip any ${VAR} placeholders that weren't substituted — fall back to env raw = raw.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, function (m, name) { - return process.env[name] || ''; + var val = process.env[name] || ''; + return JSON.stringify(val).slice(1, -1); }); return JSON.parse(raw); } @@ -254,6 +255,7 @@ function ensureDeviceGroup(db, domain, userid, cb) { db.Get(userid, function (getErr, userDocs) { if (getErr || !userDocs || userDocs.length !== 1) { // Not fatal — mesh is created, just couldn't update user links + err('Could not update user links for ' + userid + ' after creating mesh ' + meshid + (getErr ? (': ' + getErr) : '')); return cb(null, meshid); } var user = userDocs[0]; @@ -275,6 +277,10 @@ function writeMeshIdFiles(meshid, serverIdHex) { var parts = meshid.split('/'); var base64Hash = parts[parts.length - 1]; + if (base64Hash.indexOf('..') !== -1 || base64Hash.indexOf('/') !== -1 || base64Hash.indexOf('\\') !== -1) { + throw new Error('Refusing to write mesh id files: derived base64Hash contains unsafe path characters: ' + base64Hash); + } + fs.writeFileSync(path.join(MESH_DIR, 'mesh_device_group_id'), base64Hash); log('Wrote mesh_device_group_id: ' + base64Hash); @@ -282,6 +288,11 @@ function writeMeshIdFiles(meshid, serverIdHex) { var standardBase64 = base64Hash.replace(/@/g, '+').replace(/\$/g, '/'); var hex = Buffer.from(standardBase64, 'base64').toString('hex').toUpperCase(); var meshIdHex = '0x' + hex; + + if (meshIdHex.indexOf('..') !== -1 || meshIdHex.indexOf('/') !== -1 || meshIdHex.indexOf('\\') !== -1) { + throw new Error('Refusing to write mesh id files: derived meshIdHex contains unsafe path characters: ' + meshIdHex); + } + fs.writeFileSync(path.join(MESH_DIR, 'mesh_id'), meshIdHex); log('Wrote mesh_id: ' + meshIdHex); @@ -294,6 +305,10 @@ function writeMeshIdFiles(meshid, serverIdHex) { // --- Step 7: Generate meshagent.msh file --- function generateMshFile(meshIdHex, serverIdHex) { + if (meshIdHex.indexOf('..') !== -1 || meshIdHex.indexOf('/') !== -1 || meshIdHex.indexOf('\\') !== -1) { + throw new Error('Refusing to generate msh file: meshIdHex contains unsafe path characters: ' + meshIdHex); + } + var meshServerUrl; if (OPENFRAME_MODE === 'true' && OPENFRAME_GATEWAY_URL) { log('OpenFrame mode enabled — using gateway URL'); diff --git a/plugins/openframe.js b/plugins/openframe.js index 6a3a1a4b7b..c70e95ac1d 100644 --- a/plugins/openframe.js +++ b/plugins/openframe.js @@ -6,6 +6,11 @@ const path = require('path'); const MESH_DIR = process.env.MESH_DIR || '/opt/mesh'; const MESH_DEVICE_GROUP = process.env.MESH_DEVICE_GROUP || ''; +// RFC 1123 hostname (with optional port), or a bare IPv4 address (with optional port). +// This deliberately rejects control characters, whitespace, newlines and other +// characters that could inject additional MSH directives into the generated file. +const HOST_PATTERN = /^[A-Za-z0-9]([A-Za-z0-9-]{0,62}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,62}[A-Za-z0-9])?)*(:[0-9]{1,5})?$/; + // --- Helpers --- function corsHeaders(res) { @@ -59,6 +64,12 @@ module.exports.openframe = function (pluginHandler) { app.get('/generate-msh', function (req, res) { corsHeaders(res); + // Require an authenticated MeshCentral session, consistent with other endpoints (e.g. + // amt-ider.js), since this generates a downloadable agent config tied to server identity. + if (req.session == null || !req.session.userid) { + return sendError(res, 401, 'Authentication required'); + } + var host = req.query.host; if (!host) return sendError(res, 400, 'Missing required parameter: host'); @@ -74,6 +85,13 @@ module.exports.openframe = function (pluginHandler) { var protocol = host.startsWith('http://') ? 'ws' : 'wss'; var cleanHost = host.replace(/^https?:\/\//, '').replace(/^wss?:\/\//, ''); + + // Validate the stripped host: must be a plain hostname/IPv4 with optional port and no + // newlines, whitespace or other characters that could inject extra MSH directives. + if (!HOST_PATTERN.test(cleanHost)) { + return sendError(res, 400, 'Invalid host parameter'); + } + var meshServerUrl = protocol + '://' + cleanHost + '/ws/tools/agent/meshcentral-server/agent.ashx'; var mshContent = [ @@ -113,6 +131,10 @@ module.exports.openframe = function (pluginHandler) { // 1. Verify device exists in DB db.Get(nodeId, function (err, docs) { + if (err) { + log('db.Get error for ' + nodeId + ': ' + err); + return sendError(res, 500, 'Database error'); + } if (docs == null || docs.length !== 1) return sendError(res, 404, 'Device not found'); // 2. Live connectivity state from MeshCentral in-memory store @@ -121,6 +143,10 @@ module.exports.openframe = function (pluginHandler) { // 3. Last connection record from DB db.Get('lc' + nodeId, function (err, docs) { + if (err) { + log('db.Get error for lc' + nodeId + ': ' + err); + return sendError(res, 500, 'Database error'); + } var lc = (docs != null && docs.length === 1) ? docs[0] : null; res.json({