Sanitize inputs to prevent path traversal and data disclosure

This commit is contained in:
Timo Wagner committed 2025-10-25 16:15:17 +02:00
1 parent 6f3a182328
commit f84f5f8ad3
9 files changed
+122 -51

No files matched your search

+23 -12
View File
@@ -1,15 +1,7 @@
import fs from 'fs';
import config from '../config.js';
import cli from './cli.js';
/** Generate an uuid
* @url https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript#2117523 **/
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
import { checkAndSanitizeFilePathName, uuidv4 } from './utils.js';
/**
*
@@ -283,7 +275,8 @@ export default class admin {
socket.on("admin.createBundle", function (data) {
try {
if (!fs.existsSync("data/bundles" + data.dir)) {
data.dir = checkAndSanitizeFilePathName(data.dir)
if (!fs.existsSync("data/bundles/" + data.dir)) {
fs.mkdirSync("data/bundles/" + data.dir);
fs.mkdirSync("data/bundles/" + data.dir + "/images");
fs.mkdirSync("data/bundles/" + data.dir + "/render");
@@ -553,6 +546,8 @@ export default class admin {
try {
data.bundleName = checkAndSanitizeFilePathName(data.bundleName);
filename = checkAndSanitizeFilePathName(filename);
let bundle = self.bundleManager.getBundle(data.bundleName);
if (!bundle) {
throw "no active bundle";
@@ -606,14 +601,17 @@ export default class admin {
socket.on('edit.uploadImage', function (data) {
let filename = data.name.replace(/\.[^/.]+$/, "");
let ext = "." + data.type.replace("image/", "");
let ext = data.type.replace("image/", "");
if (filename === "") {
filename = uuidv4();
}
try {
let replace = "^data:" + data.type + ";base64,";
let re = new RegExp(replace, "g");
data.bundleName = checkAndSanitizeFilePathName(data.bundleName);
filename = checkAndSanitizeFilePathName(filename);
ext = "." + checkAndSanitizeFilePathName(ext)
fs.writeFileSync("./data/bundles/" + data.bundleName + "/images/" + filename + ext, data.imageData.replace(re, ""), "base64");
cli.success("upload of " + filename + ext);
socket.emit("callback.edit.updateFileList", {});
@@ -626,6 +624,8 @@ export default class admin {
socket.on('edit.deleteImage', function (data) {
try {
data.bundleName = checkAndSanitizeFilePathName(data.bundleName);
data.name = checkAndSanitizeFilePathName(data.name);
fs.unlinkSync("./data/bundles/" + data.bundleName + "/images/" + data.name);
cli.success("delete " + data.name);
socket.emit("callback.edit.updateFileList", {});
@@ -634,6 +634,17 @@ export default class admin {
socket.emit("callback.edit.updateFileList", {error: err});
}
});
socket.on('edit.deleteImage', function (data) {
try {
data.name = checkAndSanitizeFilePathName(data.name);
fs.unlinkSync("./data/backgrounds/" + data.name);
cli.success("delete " + data.name);
socket.emit("callback.edit.updateFileList", {});
} catch (err) {
cli.error(err, "delete image");
socket.emit("callback.edit.updateFileList", {error: err});
}
});
}); // io
}
+17 -31
View File
@@ -1,5 +1,6 @@
import fs from 'fs';
import cli from './cli.js';
import { checkAndSanitizeFilePathName, uuidv4 } from './utils.js';
/**
* @module infoscreen3/bundle
@@ -7,7 +8,7 @@ import cli from './cli.js';
export default class bundleClass {
constructor(name, data, slides) {
this.name = name;
this.name = checkAndSanitizeFilePathName(name);
this.bundleData = data;
this.bundleData['bundleName'] = this.name;
this.allSlides = slides;
@@ -28,6 +29,7 @@ export default class bundleClass {
getSlideJsonFile(fileName) {
try {
fileName = checkAndSanitizeFilePathName(fileName)
return fs.readFileSync(process.cwd() + "/data/bundles/" + this.name + "/slides/" + fileName).toString();
} catch (err) {
return "{}";
@@ -50,6 +52,7 @@ export default class bundleClass {
let newId = uuidv4();
let path = process.cwd() + "/data/bundles/" + this.name;
try {
uuid = checkAndSanitizeFilePathName(uuid)
// duplicate at filesystem
if (fs.existsSync(path + "/render/" + uuid + ".png")) {
fs.copyFileSync(path + "/render/" + uuid + ".png", path + "/render/" + newId + ".png");
@@ -63,8 +66,10 @@ export default class bundleClass {
newSlide.uuid = newId;
this.allSlides.push(newSlide);
this.save();
return;
} catch (err) {
cli.error("error while duplicating file:" + uuid, err);
return;
}
}
}
@@ -72,11 +77,13 @@ export default class bundleClass {
}
moveTo(bundleDir, uuid) {
let toPath = process.cwd() +"/data/bundles/" + bundleDir;
let path = process.cwd() + "/data/bundles/" + this.name;
for (let slide of this.allSlides) {
if (slide.uuid === uuid) {
try {
try {
bundleDir = checkAndSanitizeFilePathName(bundleDir)
uuid = checkAndSanitizeFilePathName(uuid)
let toPath = process.cwd() +"/data/bundles/" + bundleDir;
let path = process.cwd() + "/data/bundles/" + this.name;
for (let slide of this.allSlides) {
if (slide.uuid === uuid) {
// duplicate at filesystem
if (fs.existsSync(path + "/render/" + uuid + ".png")) {
fs.renameSync(path + "/render/" + uuid + ".png", toPath + "/render/" + uuid + ".png");
@@ -88,11 +95,11 @@ export default class bundleClass {
let slideData = clone(slide);
this.removeUuid(uuid);
return slideData;
} catch (err) {
cli.error("error while moving file:" + uuid, err);
return null;
}
}
} catch (err) {
cli.error("error while moving file:" + uuid, err);
return null;
}
}
@@ -111,6 +118,7 @@ export default class bundleClass {
if (slide.type === "slide") {
let path = process.cwd() + "/data/bundles/" + this.name;
try {
uuid = checkAndSanitizeFilePathName(uuid)
if (fs.existsSync(path + "/render/" + uuid + ".png")) {
fs.unlinkSync(path + "/render/" + uuid + ".png");
cli.success("removing file:" + path + "/render/" + uuid + ".png");
@@ -195,19 +203,6 @@ export default class bundleClass {
}
}
getSlideData(uuid) {
let slideData = {};
for (let slide of this.allSlides) {
if (slide.uuid === uuid) {
slideData = {
settings: slide,
render: fs.readFileSync(process.cwd() + "/data/bundles/" + this.name + "/slides/" + slide.uuid + ".png")
};
return slide;
}
}
}
save() {
this.sync();
try {
@@ -245,12 +240,3 @@ function sortByProperty(property) {
}
}
/** Generate an uuid
* @url https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript#2117523 **/
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
+3 -1
View File
@@ -1,6 +1,7 @@
import fs from 'fs';
import cli from './cli.js';
import bundle from './bundle.js';
import cli from './cli.js';
import { checkAndSanitizeFilePathName } from './utils.js';
/**
* @module infoscreen3/bundleManager
@@ -37,6 +38,7 @@ export default class bundleManager {
return this.bundles[name];
} else {
try {
name = checkAndSanitizeFilePathName(name)
cli.info(name + " not in bundles, trying to load from fs...");
let data = getJson("./data/bundles/" + name + "/bundle.json");
let slides = getJson("./data/bundles/" + name + "/slides.json");
+2
View File
@@ -1,5 +1,6 @@
import fs from 'fs';
import cli from './cli.js';
import { checkAndSanitizeFilePathName } from './utils.js';
/**
* handles one display instance
@@ -253,6 +254,7 @@ export default class display {
this.serverOptions.loop = false;
this.serverOptions.isAnnounce = true;
this.serverOptions.announceMeta = json;
this.serverOptions.displayId = checkAndSanitizeFilePathName(this.serverOptions.displayId)
// save temporarily png data...
if (json.type === "image") {
fs.writeFileSync("./tmp/display_" + this.serverOptions.displayId + ".png", pngData.replace(/^data:image\/png;base64,/, ""), "base64");
+4 -2
View File
@@ -1,6 +1,7 @@
import express from 'express';
import Twig from 'twig';
import fs from 'fs';
import Twig from 'twig';
import { checkAndSanitizeFilePathName } from './utils.js';
/**
* Base plugin class
@@ -15,7 +16,7 @@ import fs from 'fs';
export default class plugin {
constructor(pluginName, app, io, dispatcher) {
this.pluginName = pluginName;
this.pluginName = checkAndSanitizeFilePathName(pluginName);
this.subApp = express();
this.subApp.set('views', "plugins/" + pluginName + "/views");
this.subApp.set('view engine', 'twig');
@@ -32,6 +33,7 @@ export default class plugin {
}
renderPluginView(template, data) {
template = checkAndSanitizeFilePathName(template)
return Twig.twig({ data: fs.readFileSync("plugins/" + this.pluginName + "/views/" + template + ".twig").toString() }).render(data);
}
+18
View File
@@ -0,0 +1,18 @@
import sanitize from 'sanitize-filename';
/** Generate an uuid
* @url https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript#2117523 **/
export function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// Helper function to throw an Exception in case an invalid File or Dir name is given
export function checkAndSanitizeFilePathName(path) {
const newPath = sanitize(path);
if (newPath !== path) {
throw 'Given File or Bundlename contains invalid characters.';
}
return newPath;
}
+46
View File
@@ -35,6 +35,7 @@
"passport-local": "1.0.0",
"passport.socketio": "3.7.0",
"rate-limiter-flexible": "2.4.1",
"sanitize-filename": "^1.6.3",
"socket.io": "4.8.1",
"sqlite3": "5.1.5",
"twig": "1.15.4",
@@ -6331,6 +6332,15 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sanitize-filename": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
"license": "WTFPL OR ISC",
"dependencies": {
"truncate-utf8-bytes": "^1.0.0"
}
},
"node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
@@ -6890,6 +6900,15 @@
"node": ">=0.10.0"
}
},
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
"license": "WTFPL",
"dependencies": {
"utf8-byte-length": "^1.0.1"
}
},
"node_modules/tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
@@ -7074,6 +7093,12 @@
"node": ">=6.14.2"
}
},
"node_modules/utf8-byte-length": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
"integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
"license": "(WTFPL OR MIT)"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -12152,6 +12177,14 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sanitize-filename": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
"requires": {
"truncate-utf8-bytes": "^1.0.0"
}
},
"schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
@@ -12544,6 +12577,14 @@
"integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==",
"dev": true
},
"truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
"requires": {
"utf8-byte-length": "^1.0.1"
}
},
"tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
@@ -12670,6 +12711,11 @@
"node-gyp-build": "^4.3.0"
}
},
"utf8-byte-length": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
"integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+1
View File
@@ -49,6 +49,7 @@
"passport-local": "1.0.0",
"passport.socketio": "3.7.0",
"rate-limiter-flexible": "2.4.1",
"sanitize-filename": "^1.6.3",
"socket.io": "4.8.1",
"sqlite3": "5.1.5",
"twig": "1.15.4",
+8 -5
View File
@@ -1,11 +1,12 @@
import busboy from 'connect-busboy';
import express from 'express';
const router = express.Router();
import cli from '../modules/cli.js';
import fs from 'fs';
import fse from 'fs-extra';
import path from 'path';
import config from '../config.js';
import busboy from 'connect-busboy'
import fse from 'fs-extra';
import cli from '../modules/cli.js';
import { checkAndSanitizeFilePathName } from '../modules/utils.js';
const router = express.Router();
function getFiles(path) {
@@ -93,6 +94,7 @@ export default function (pluginManager, websocket, dispatcher) {
req.busboy.on('file', function (fieldname, file, details) {
if (fieldname === "newBackground")
if (details.filename && details.filename.length > 0 && (details.mimeType === "image/jpeg" || details.mimeType === "video/mp4" || details.mimeType === "image/png")) {
details.filename = checkAndSanitizeFilePathName(details.filename)
let fstream = fs.createWriteStream('./data/backgrounds/' + details.filename);
file.pipe(fstream);
} else {
@@ -164,6 +166,7 @@ export default function (pluginManager, websocket, dispatcher) {
req.busboy.on('finish', function () {
if (fields.hasOwnProperty("sDelete")) {
fields.bundle = checkAndSanitizeFilePathName(fields.bundle)
let dir = `./data/bundles/${fields.bundle}/`;
if (fs.existsSync(dir)) {
let trashDir = `./trash/${fields.bundle}/`;
@@ -260,7 +263,7 @@ export default function (pluginManager, websocket, dispatcher) {
});
router.get('/ajax/imagelist', function (req, res, next) {
let bundle = req.query['bundle'];
let bundle = checkAndSanitizeFilePathName(req.query['bundle']);
let replaceMode = req.query['replacemode'];
let bundleRoot = path.normalize("./data/bundles/" + bundle);
let bundleImages = fs.readdirSync(bundleRoot + "/images", {