mirror of
https://github.com/UiO-Gaming/infoscreen3.git
synced 2026-11-05 13:56:52 +01:00
Sanitize inputs to prevent path traversal and data disclosure
This commit is contained in:
1 parent
6f3a182328
commit
f84f5f8ad3
9 files changed
+122
-51
No files matched your search
+23
-12
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Generated
+46
@@ -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",
|
||||
|
||||
@@ -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
@@ -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", {
|
||||
|
||||
Reference in new issue
Block a user