Skip to content

Commit f6263e9

Browse files
authored
Merge pull request #7491 from Countly/codex/fix-data-migration-path-traversal
Fix data migration path traversal
2 parents c9c4322 + 26b9dfe commit f6263e9

13 files changed

Lines changed: 361 additions & 68 deletions

File tree

api/parts/data/exports.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,15 @@ exports.output = function(params, data, filename, type) {
208208

209209
if (type === "xlsx" || type === "xls") { //we have stream
210210
params.res.writeHead(200, headers);
211+
data.on("error", function(streamErr) {
212+
common.log("exports").e(streamErr);
213+
if (!params.res.headersSent) {
214+
common.returnMessage(params, 500, "Export stream error");
215+
}
216+
else {
217+
params.res.end();
218+
}
219+
});
211220
data.pipe(params.res);
212221
//common.returnRaw(params, 200, new Buffer(data, 'binary'), headers);
213222
}
@@ -403,6 +412,15 @@ exports.stream = function(params, stream, options) {
403412
else if (type === 'xlsx' || type === 'xls') {
404413
options.streamOptions.transform = transformFunction;
405414
var xc = new XLSXTransformStream();
415+
xc.on("error", function(streamErr) {
416+
common.log("exports").e(streamErr);
417+
if (!params.res.headersSent) {
418+
common.returnMessage(params, 500, "Export stream error");
419+
}
420+
else {
421+
params.res.end();
422+
}
423+
});
406424
xc.pipe(params.res);
407425
if (listAtEnd === false) {
408426
xc.write(paramList);

api/utils/common.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const mongodb = require('mongodb');
2525
const getRandomValues = require('get-random-values');
2626
const semver = require('semver');
2727
const _ = require('lodash');
28+
const path = require('path');
2829

2930
var matchHtmlRegExp = /"|'|&(?!amp;|quot;|#39;|lt;|gt;|#46;|#36;)|<|>/;
3031
var matchLessHtmlRegExp = /[<>]/;
@@ -2345,9 +2346,9 @@ common.clearClashingQueryOperations = function(query) {
23452346
}
23462347
}
23472348

2348-
for (var path in map) {
2349-
if (map[path] > 1) {
2350-
badPaths.push(path);
2349+
for (var fieldPath in map) {
2350+
if (map[fieldPath] > 1) {
2351+
badPaths.push(fieldPath);
23512352
}
23522353
}
23532354
if (badPaths.length > 0) {
@@ -2819,6 +2820,25 @@ common.sanitizeFilename = (filename, replacement = "") => {
28192820
.replace(/^\.+/, replacement);
28202821
};
28212822

2823+
/**
2824+
* Resolve an input path under a base directory and reject path traversal.
2825+
* @param {string} basePath - base directory for allowed paths
2826+
* @param {string} inputPath - user-supplied path segment or relative path
2827+
* @returns {string|null} contained absolute path or null
2828+
*/
2829+
common.resolvePathInBase = (basePath, inputPath) => {
2830+
basePath = path.resolve(basePath + "");
2831+
inputPath = (inputPath + "").replace(/\\/g, "/");
2832+
while (inputPath.indexOf("/") === 0) {
2833+
inputPath = inputPath.substring(1);
2834+
}
2835+
let resolvedPath = path.resolve(basePath, inputPath);
2836+
if (resolvedPath === basePath || resolvedPath.indexOf(basePath + path.sep) === 0) {
2837+
return resolvedPath;
2838+
}
2839+
return null;
2840+
};
2841+
28222842
common.sanitizeHTML = (html, extendedWhitelist) => {
28232843
const whiteList = {
28242844
a: ["target", "title"],
@@ -3499,4 +3519,4 @@ common.trimWhitespaceStartEnd = function(value) {
34993519
};
35003520

35013521
/** @type {import('../../types/common').Common} */
3502-
module.exports = common;
3522+
module.exports = common;

api/utils/requestProcessor.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,15 @@ const processRequest = (params) => {
16831683
common.returnMessage(params, 400, "Export doesn't exist");
16841684
}
16851685
else {
1686+
stream.on("error", function(streamErr) {
1687+
log.e(streamErr);
1688+
if (!params.res.headersSent) {
1689+
common.returnMessage(params, 500, "Export stream error");
1690+
}
1691+
else {
1692+
params.res.end();
1693+
}
1694+
});
16861695
params.res.writeHead(200, {
16871696
'Content-Type': 'application/x-gzip',
16881697
'Content-Length': size,
@@ -2260,6 +2269,15 @@ const processRequest = (params) => {
22602269
common.returnMessage(params, 400, "Export stream does not exist");
22612270
}
22622271
else {
2272+
stream.on("error", function(streamErr) {
2273+
log.e(streamErr);
2274+
if (!params.res.headersSent) {
2275+
common.returnMessage(params, 500, "Export stream error");
2276+
}
2277+
else {
2278+
params.res.end();
2279+
}
2280+
});
22632281
headers = {};
22642282
headers["Content-Type"] = countlyApi.data.exports.getType(type);
22652283
headers["Content-Disposition"] = "attachment;filename=" + encodeURIComponent(filename);

frontend/express/app.js

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -502,16 +502,30 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
502502
res.sendFile(__dirname + '/public/images/default_app_icon.png');
503503
}
504504
else {
505-
countlyFs.getStats("appimages", __dirname + '/public/appimages/' + req.params[0], {id: req.params[0]}, function(err, stats) {
505+
var appImagePath = common.resolvePathInBase(__dirname + '/public/appimages', req.params[0]);
506+
if (!appImagePath) {
507+
res.sendFile(__dirname + '/public/images/default_app_icon.png');
508+
return;
509+
}
510+
countlyFs.getStats("appimages", appImagePath, {id: req.params[0]}, function(err, stats) {
506511
if (err || !stats || !stats.size) {
507512
res.sendFile(__dirname + '/public/images/default_app_icon.png');
508513
}
509514
else {
510-
countlyFs.getStream("appimages", __dirname + '/public/appimages/' + req.params[0], {id: req.params[0]}, function(err2, stream) {
515+
countlyFs.getStream("appimages", appImagePath, {id: req.params[0]}, function(err2, stream) {
511516
if (err2 || !stream) {
512517
res.sendFile(__dirname + '/public/images/default_app_icon.png');
513518
}
514519
else {
520+
stream.on('error', function(streamErr) {
521+
log.e(streamErr);
522+
if (!res.headersSent) {
523+
res.sendFile(__dirname + '/public/images/default_app_icon.png');
524+
}
525+
else {
526+
res.end();
527+
}
528+
});
515529
res.writeHead(200, {
516530
'Accept-Ranges': 'bytes',
517531
'Cache-Control': 'public, max-age=31536000',
@@ -536,16 +550,30 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
536550
res.sendFile(__dirname + '/public/images/default_member_icon.png');
537551
}
538552
else {
539-
countlyFs.getStats("memberimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err, stats) {
553+
var memberImagePath = common.resolvePathInBase(__dirname + '/public/memberimages', req.params[0]);
554+
if (!memberImagePath) {
555+
res.sendFile(__dirname + '/public/images/default_member_icon.png');
556+
return;
557+
}
558+
countlyFs.getStats("memberimages", memberImagePath, {id: req.params[0]}, function(err, stats) {
540559
if (err || !stats || !stats.size) {
541560
res.sendFile(__dirname + '/public/images/default_member_icon.png');
542561
}
543562
else {
544-
countlyFs.getStream("memberimages", __dirname + '/public/' + req.path, {id: req.params[0]}, function(err2, stream) {
563+
countlyFs.getStream("memberimages", memberImagePath, {id: req.params[0]}, function(err2, stream) {
545564
if (err2 || !stream) {
546565
res.sendFile(__dirname + '/public/images/default_member_icon.png');
547566
}
548567
else {
568+
stream.on('error', function(streamErr) {
569+
log.e(streamErr);
570+
if (!res.headersSent) {
571+
res.sendFile(__dirname + '/public/images/default_member_icon.png');
572+
}
573+
else {
574+
res.end();
575+
}
576+
});
549577
res.writeHead(200, {
550578
'Accept-Ranges': 'bytes',
551579
'Cache-Control': 'public, max-age=31536000',
@@ -564,15 +592,26 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
564592
});
565593

566594
app.get(countlyConfig.path + "*/screenshots/*", function(req, res) {
567-
countlyFs.getStats("screenshots", __dirname + '/public/' + req.path, {id: "core"}, function(err, stats) {
595+
var screenshotPath = common.resolvePathInBase(__dirname + '/public', req.path);
596+
if (!screenshotPath) {
597+
return res.send(false);
598+
}
599+
countlyFs.getStats("screenshots", screenshotPath, {id: "core"}, function(err, stats) {
568600
if (err || !stats || !stats.size) {
569601
return res.send(false);
570602
}
571603

572-
countlyFs.getStream("screenshots", __dirname + '/public/' + req.path, {id: "core"}, function(err2, stream) {
604+
countlyFs.getStream("screenshots", screenshotPath, {id: "core"}, function(err2, stream) {
573605
if (err2 || !stream) {
574606
return res.send(false);
575607
}
608+
stream.on('error', function(streamErr) {
609+
log.e(streamErr);
610+
if (!res.headersSent) {
611+
return res.send(false);
612+
}
613+
res.end();
614+
});
576615

577616
res.writeHead(200, {
578617
'Accept-Ranges': 'bytes',

plugins/crashes/api/api.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,15 @@ plugins.setConfigs("crashes", {
12811281
'Content-Disposition': "attachment;filename=" + encodeURIComponent(params.qstring.crash_id) + "_bin.dmp"
12821282
});
12831283
let stream = new Duplex();
1284+
stream.on("error", function(streamErr) {
1285+
log.e(streamErr);
1286+
if (!params.res.headersSent) {
1287+
common.returnMessage(params, 500, "Binary stream error");
1288+
}
1289+
else {
1290+
params.res.end();
1291+
}
1292+
});
12841293
stream.push(buf);
12851294
stream.push(null);
12861295
stream.pipe(params.res);

plugins/dashboards/frontend/app.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ var plugins = require('../../pluginManager.js');
22
var exported = {};
33
var countlyConfig = require('../../../frontend/express/config', 'dont-enclose');
44
var countlyFs = require('../../../api/utils/countlyFs.js');
5+
var common = require('../../../api/utils/common.js');
6+
var path = require('path');
7+
var log = common.log('dashboards:frontend');
58

69
(function(plugin) {
710
plugin.init = function(/*app, countlyDb*/) {
@@ -18,23 +21,30 @@ var countlyFs = require('../../../api/utils/countlyFs.js');
1821
if (!req || !req.params) {
1922
return res.send(false);
2023
}
21-
var requestPath = req.path;
22-
var reqArray = requestPath.split("/");
2324
var fileName = "";
24-
if (reqArray && reqArray.length) {
25-
reqArray[0] += "/frontend/public";
26-
fileName = reqArray[reqArray.length - 1];
25+
if (req.params && req.params[0]) {
26+
fileName = req.params[0];
2727
}
28-
requestPath = reqArray.join("/");
29-
countlyFs.getStats("screenshots", __dirname + '/../../../plugins/' + requestPath, {id: "dashboards/" + fileName}, function(err, stats) {
28+
var requestPath = common.resolvePathInBase(path.resolve(__dirname, './public/images/screenshots'), fileName);
29+
if (!requestPath) {
30+
return res.send(false);
31+
}
32+
countlyFs.getStats("screenshots", requestPath, {id: "dashboards/" + fileName}, function(err, stats) {
3033
if (err || !stats || !stats.size) {
3134
return res.send(false);
3235
}
3336

34-
countlyFs.getStream("screenshots", __dirname + '/../../../plugins/' + requestPath, {id: "dashboards/" + fileName}, function(err2, stream) {
37+
countlyFs.getStream("screenshots", requestPath, {id: "dashboards/" + fileName}, function(err2, stream) {
3538
if (err2 || !stream) {
3639
return res.send(false);
3740
}
41+
stream.on('error', function(streamErr) {
42+
log.e(streamErr);
43+
if (!res.headersSent) {
44+
return res.send(false);
45+
}
46+
res.end();
47+
});
3848

3949
res.writeHead(200, {
4050
'Accept-Ranges': 'bytes',
@@ -52,4 +62,4 @@ var countlyFs = require('../../../api/utils/countlyFs.js');
5262
};
5363
}(exported));
5464

55-
module.exports = exported;
65+
module.exports = exported;

0 commit comments

Comments
 (0)