From 68f63b4f2d4dfca55315f9cdde5c0ba3418e6f3b Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Wed, 25 Sep 2024 12:41:56 -0700 Subject: [PATCH 1/4] Fixed filenames not being escaped when editing files This allowed a possible XSS by naming a file in a particular way on your device. --- views/default.handlebars | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/default.handlebars b/views/default.handlebars index d95d349f50..bf91052b13 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -11556,7 +11556,7 @@ gdownloadFile = { path: decodeURIComponent(x), file: decodeURIComponent(y), size: z, tsize: 0, data: '', state: 0, id: Math.random(), tag: tag } //console.log('p13downloadFileCancel', gdownloadFile); files.sendText({ action: 'download', sub: 'start', id: gdownloadFile.id, path: gdownloadFile.path }); - setDialogMode(2, "Download File", 10, p13downloadFileCancel, '
' + gdownloadFile.file + '

'); + setDialogMode(2, "Download File", 10, p13downloadFileCancel, '
' + EscapeHtml(gdownloadFile.file) + '

'); } // Called by the html page to cancel the download @@ -11727,7 +11727,7 @@ if (uploadFile.xfiles.length > uploadFile.xfilePtr) { uploadFile.xptr = 0; var file = uploadFile.xfiles[uploadFile.xfilePtr]; - QH('p13dfileName', file.name); + QH('p13dfileName', EscapeHtml(file.name)); Q('d2progressBar').max = file.size; Q('d2progressBar').value = 0; if (file.xdata == null) { From 815c242f30c91b3ba64f1de1ee2fd3ced5394fbd Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Wed, 25 Sep 2024 12:43:21 -0700 Subject: [PATCH 2/4] Fixed HTML generation in webserver not escaping most things from req.query This would allow XSS through a very simple phishing attack --- webserver.js | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/webserver.js b/webserver.js index 21abc4801d..8e92d79d55 100644 --- a/webserver.js +++ b/webserver.js @@ -814,6 +814,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF return parent.config.domains['']; } + function cleanReqQuery(req, res) { + + } + function handleLogoutRequest(req, res) { const domain = checkUserIpAddress(req, res); if (domain == null) { return; } @@ -861,7 +865,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } // This is the default logout redirect to the login page - if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + req.query.key); } else { res.redirect(domain.url + 'login'); } + if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + encodeURIComponent(req.query.key)); } else { res.redirect(domain.url + 'login'); } } // Return an object with 2FA type if 2-step auth can be skipped @@ -2081,7 +2085,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF for (var i in obj.meshes) { if ((obj.meshes[i].domain == domain.id) && (obj.meshes[i].deleted == null) && (obj.meshes[i].invite != null) && (obj.meshes[i].invite.codes.indexOf(req.body.inviteCode) >= 0)) { // Send invitation link, valid for 1 minute. - res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, ag: obj.meshes[i].invite.ag, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + req.query.key) : '') + (req.query.hide ? ('&hide=' + req.query.hide) : '')); + res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, ag: obj.meshes[i].invite.ag, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + (req.query.hide ? ('&hide=' + encodeURIComponent(req.query.hide)) : '')); return; } } @@ -3027,7 +3031,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // If the request has a "meshmessengerid", redirect to MeshMessenger // This situation happens when you get a push notification for a chat session, but are not logged in. if (req.query.meshmessengerid != null) { - res.redirect(domain.url + 'messenger?id=' + req.query.meshmessengerid + ((req.query.key != null) ? ('&key=' + req.query.key) : '')); + res.redirect(domain.url + 'messenger?id=' + encodeURIComponent(req.query.meshmessengerid) + ((req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '')); return; } @@ -3507,7 +3511,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; } var logoutcontrols = { name: user.name }; - var extras = (req.query.key != null) ? ('&key=' + req.query.key) : ''; + var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button // Create a authentication cookie @@ -3564,7 +3568,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain var user = obj.users[req.session.userid]; var logoutcontrols = { name: user.name }; - var extras = (req.query.key != null) ? ('&key=' + req.query.key) : ''; + var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain)); } else { @@ -3583,7 +3587,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain var user = obj.users[req.session.userid]; var logoutcontrols = { name: user.name }; - var extras = (req.query.key != null) ? ('&key=' + req.query.key) : ''; + var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain)); } else { @@ -3598,7 +3602,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain var user = obj.users[req.session.userid]; var logoutcontrols = { name: user.name }; - var extras = (req.query.key != null) ? ('&key=' + req.query.key) : ''; + var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain)); } else { @@ -3630,7 +3634,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6] if (!req.session || !req.session.userid) { // Redirect to login page - if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key + '&meshmessengerid=' + req.query.id); } else { res.redirect(domain.url + '?meshmessengerid=' + req.query.id); } + if (req.query.key != null) { res.redirect(domain.url + '?key=' + encodeURIComponent(req.query.key) + '&meshmessengerid=' + encodeURIComponent(req.query.id)); } else { res.redirect(domain.url + '?meshmessengerid=' + encodeURIComponent(req.query.id)); } return; } if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; } @@ -5480,7 +5484,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF meshsettings += 'MeshServer=local\r\n'; if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; } } - if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; } + if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; } if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; } } if (req.query.id == '10006') { // Assistant settings and customizations @@ -5769,9 +5773,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF agentinfo: agentinfo, filestats: filestats, currentAgent: agentinfo.hashhex.startsWith(fileSplit[1].toLowerCase()), - downloadUrl: req.originalUrl.split('?')[0] + '?dldump=' + file + (req.query.key ? ('&key=' + req.query.key) : ''), - deleteUrl: req.originalUrl.split('?')[0] + '?deldump=' + file + (req.query.key ? ('&key=' + req.query.key) : ''), - agentUrl: req.originalUrl.split('?')[0] + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + req.query.key) : ''), + downloadUrl: req.originalUrl.split('?')[0] + '?dldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''), + deleteUrl: req.originalUrl.split('?')[0] + '?deldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''), + agentUrl: req.originalUrl.split('?')[0] + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''), time: new Date(filestats.ctime) }); } @@ -5787,7 +5791,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF response += '' + d.fileSplit[1].toLowerCase() + '' + d.fileSplit[2] + 'Delete'; } } - response += 'Mesh Agents'; + response += 'Mesh Agents'; res.send(response); return; } @@ -5798,9 +5802,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var response = 'Mesh Agents Cores'; response += ''; for (var i in parent.defaultMeshCores) { - response += ''; + response += ''; } - response += '
NameSizeCompDecompressed Hash SHA384
' + i.split(' ').join(' ') + '' + parent.defaultMeshCores[i].length + (req.query.key ? ('?key=' + req.query.key) : '') + '' + parent.defaultMeshCoresDeflate[i].length + '' + Buffer.from(parent.defaultMeshCoresHash[i], 'binary').toString('hex') + '
' + i.split(' ').join(' ') + '' + parent.defaultMeshCores[i].length + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '' + parent.defaultMeshCoresDeflate[i].length + '' + Buffer.from(parent.defaultMeshCoresHash[i], 'binary').toString('hex') + '
Mesh Agents'; + response += 'Mesh Agents'; res.send(response); return; } @@ -5809,7 +5813,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Download mesh core var bin = parent.defaultMeshCores[req.query.dlcore]; if ((bin == null) || (bin.length < 5)) { try { res.sendStatus(404); } catch (ex) { } return; } - setContentDispositionHeader(res, 'application/octet-stream', req.query.dlcore + '.js', null, 'meshcore.js'); + setContentDispositionHeader(res, 'application/octet-stream', encodeURIComponent(req.query.dlcore) + '.js', null, 'meshcore.js'); res.send(bin.slice(4)); return; } @@ -5832,18 +5836,18 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var agentinfo = obj.parent.meshAgentBinaries[agentid]; if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; } response += '' + agentinfo.id + '' + agentinfo.desc.split(' ').join(' ') + ''; - response += '' + agentinfo.rname + ''; + response += '' + agentinfo.rname + ''; if ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0))) { - if ((agentid == 3) || (agentid == 4)) { response += ', PDB'; } + if ((agentid == 3) || (agentid == 4)) { response += ', PDB'; } } - if (agentinfo.zdata != null) { response += ', ZIP'; } + if (agentinfo.zdata != null) { response += ', ZIP'; } response += ''; response += '' + agentinfo.size + '' + agentinfo.hashhex + ''; - response += '' + agentinfo.rname.replace('agent', 'cmd') + ''; + response += '' + agentinfo.rname.replace('agent', 'cmd') + ''; } response += ''; - response += 'MeshCores '; - if (coreDumpsAllowed) { response += 'MeshAgent Crash Dumps'; } + response += 'MeshCores '; + if (coreDumpsAllowed) { response += 'MeshAgent Crash Dumps'; } response += ''; res.send(response); return; @@ -5915,7 +5919,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF meshsettings += 'MeshServer=local\r\n'; if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; } } - if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; } + if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; } if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; } if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; } if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } } @@ -6063,7 +6067,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF meshsettings += 'MeshServer=local\r\n'; if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; } } - if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; } + if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; } if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; } if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; } if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } } From e82008cc23b81110eb34afd5071c0c7be72491cf Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Wed, 25 Sep 2024 12:48:24 -0700 Subject: [PATCH 3/4] Added HtmlEscape to Mobile default as well --- views/default-mobile.handlebars | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/default-mobile.handlebars b/views/default-mobile.handlebars index fc316db267..6e36219c04 100644 --- a/views/default-mobile.handlebars +++ b/views/default-mobile.handlebars @@ -5895,7 +5895,7 @@ downloadFile = { path: decodeURIComponent(x), file: decodeURIComponent(y), size: z, tsize: 0, data: '', state: 0, id: Math.random() } //console.log('p13downloadFileCancel', downloadFile); files.sendText({ action: 'download', sub: 'start', id: downloadFile.id, path: downloadFile.path }); - setDialogMode(2, "Download File", 10, p13downloadFileCancel, '
' + downloadFile.file + '

'); + setDialogMode(2, "Download File", 10, p13downloadFileCancel, '
' + HtmlEscape(downloadFile.file) + '

'); } // Called by the html page to cancel the download @@ -6043,7 +6043,7 @@ if (uploadFile.xfiles.length > uploadFile.xfilePtr) { uploadFile.xptr = 0; var file = uploadFile.xfiles[uploadFile.xfilePtr]; - QH('p13dfileName', file.name); + QH('p13dfileName', HtmlEscape(file.name)); Q('d2progressBar').max = file.size; Q('d2progressBar').value = 0; if (file.xdata == null) { From 1c375149623b43127955a3b2c69dbf38713235ab Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Wed, 25 Sep 2024 13:17:55 -0700 Subject: [PATCH 4/4] Added sanitization to SAML redirect and Twitter/Azure --- webserver.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webserver.js b/webserver.js index 8e92d79d55..64e5aec364 100644 --- a/webserver.js +++ b/webserver.js @@ -2820,7 +2820,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF //res.redirect(domain.url); // This does not handle cookie correctly. res.set('Content-Type', 'text/html'); let url = domain.url; - if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return key + "=" + encodeURIComponent(req.query[key]); }).join("&"); } + if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); } res.end(''); } @@ -6785,7 +6785,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var url = req.url; if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop. res.set('Content-Type', 'text/html'); - res.end(''); + res.end(''); } else { domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); }); } @@ -6835,7 +6835,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var url = req.url; if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop. res.set('Content-Type', 'text/html'); - res.end(''); + res.end(''); } else { if (req.query.state != null) { var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout