Merge pull request #40 from DumbWareio/fix/sanitize-file-names

Add filename sanitization on upload on front and backend
This commit is contained in:
Chris 2025-05-30 18:41:19 -07:00 committed by GitHub
commit f65bee9453
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 26 deletions

View File

@ -1,6 +1,8 @@
// public/managers/import.js
// ImportManager handles all import modal logic, file selection, mapping, and import actions
import { sanitizeFileName } from '/src/services/fileUpload/utils.js';
export class ImportManager {
constructor({
importModal,
@ -47,7 +49,7 @@ export class ImportManager {
if (!file) return;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('file', new File([file], sanitizeFileName(file.name), { type: file.type }));
const response = await fetch('/api/import-assets', {
method: 'POST',
body: formData,
@ -173,7 +175,7 @@ export class ImportManager {
// ...existing code for sending to backend...
try {
const formData = new FormData();
formData.append('file', file);
formData.append('file', new File([file], sanitizeFileName(file.name), { type: file.type }));
formData.append('mappings', JSON.stringify(mappings));
const response = await fetch('/api/import-assets', {
method: 'POST',

View File

@ -22,6 +22,7 @@ const { startWarrantyCron } = require('./src/services/notifications/warrantyCron
const { generatePWAManifest } = require("./scripts/pwa-manifest-generator");
const { originValidationMiddleware, getCorsOptions } = require('./middleware/cors');
const { demoModeMiddleware } = require('./middleware/demo');
const { sanitizeFileName } = require('./src/services/fileUpload/utils');
const packageJson = require('./package.json');
const app = express();
@ -946,7 +947,8 @@ const imageStorage = multer.diskStorage({
cb(null, path.join(DATA_DIR, 'Images'));
},
filename: (req, file, cb) => {
cb(null, `${uuidv4()}${path.extname(file.originalname)}`);
const safeName = sanitizeFileName(file.originalname);
cb(null, `${uuidv4()}${path.extname(safeName)}`);
}
});
@ -955,7 +957,8 @@ const receiptStorage = multer.diskStorage({
cb(null, path.join(DATA_DIR, 'Receipts'));
},
filename: (req, file, cb) => {
cb(null, `${uuidv4()}${path.extname(file.originalname)}`);
const safeName = sanitizeFileName(file.originalname);
cb(null, `${uuidv4()}${path.extname(safeName)}`);
}
});
@ -964,7 +967,8 @@ const manualStorage = multer.diskStorage({
cb(null, path.join(DATA_DIR, 'Manuals'));
},
filename: (req, file, cb) => {
cb(null, `${uuidv4()}${path.extname(file.originalname)}`);
const safeName = sanitizeFileName(file.originalname);
cb(null, `${uuidv4()}${path.extname(safeName)}`);
}
});
@ -1015,11 +1019,11 @@ app.post('/api/upload/image', uploadImage.single('photo'), (req, res) => {
const stats = fs.statSync(req.file.path);
res.json({
path: `/Images/${req.file.filename}`,
path: `/Images/${sanitizeFileName(req.file.filename)}`,
fileInfo: {
originalName: req.file.originalname,
originalName: sanitizeFileName(req.file.originalname),
size: stats.size,
fileName: req.file.filename
fileName: sanitizeFileName(req.file.filename)
}
});
});
@ -1031,11 +1035,11 @@ app.post('/api/upload/receipt', uploadReceipt.single('receipt'), (req, res) => {
const stats = fs.statSync(req.file.path);
res.json({
path: `/Receipts/${req.file.filename}`,
path: `/Receipts/${sanitizeFileName(req.file.filename)}`,
fileInfo: {
originalName: req.file.originalname,
originalName: sanitizeFileName(req.file.originalname),
size: stats.size,
fileName: req.file.filename
fileName: sanitizeFileName(req.file.filename)
}
});
});
@ -1047,11 +1051,11 @@ app.post('/api/upload/manual', uploadManual.single('manual'), (req, res) => {
const stats = fs.statSync(req.file.path);
res.json({
path: `/Manuals/${req.file.filename}`,
path: `/Manuals/${sanitizeFileName(req.file.filename)}`,
fileInfo: {
originalName: req.file.originalname,
originalName: sanitizeFileName(req.file.originalname),
size: stats.size,
fileName: req.file.filename
fileName: sanitizeFileName(req.file.filename)
}
});
});

View File

@ -3,7 +3,7 @@
* Handles file uploads, previews, and drag-and-drop functionality
*/
import { validateFileType, formatFileSize } from './utils.js';
import { validateFileType, formatFileSize, sanitizeFileName } from './utils.js';
import { createPhotoPreview, createDocumentPreview } from '../render/previewRenderer.js';
// Get access to the global flags
@ -48,7 +48,7 @@ async function uploadFile(file, type, id) {
endpoint = `${apiBaseUrl}/api/upload/receipt`;
}
const formData = new FormData();
formData.append(fieldName, file);
formData.append(fieldName, new File([file], sanitizeFileName(file.name), { type: file.type }));
formData.append('id', id);
try {
const response = await fetch(endpoint, {
@ -118,7 +118,7 @@ function setupFileInputPreview(inputId, previewId, isDocument = false, fileType
input.files = new DataTransfer().files;
validFiles.forEach(file => {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
dataTransfer.items.add(new File([file], sanitizeFileName(file.name), { type: file.type }));
input.files = dataTransfer.files;
});
input.dispatchEvent(new Event('change'));
@ -161,7 +161,7 @@ function setupFileInputPreview(inputId, previewId, isDocument = false, fileType
const dataTransfer = new DataTransfer();
Array.from(input.files).forEach((f, i) => {
if (f !== file) {
dataTransfer.items.add(f);
dataTransfer.items.add(new File([f], sanitizeFileName(f.name), { type: f.type }));
}
});
input.files = dataTransfer.files;
@ -174,7 +174,7 @@ function setupFileInputPreview(inputId, previewId, isDocument = false, fileType
};
// Use createDocumentPreview for documents with filename and size
const docPreview = createDocumentPreview(docType, file.name, deleteHandler, file.name, formatFileSize(file.size));
const docPreview = createDocumentPreview(docType, sanitizeFileName(file.name), deleteHandler, sanitizeFileName(file.name), formatFileSize(file.size));
previewItem.appendChild(docPreview);
} else {
@ -189,7 +189,7 @@ function setupFileInputPreview(inputId, previewId, isDocument = false, fileType
const dataTransfer = new DataTransfer();
Array.from(input.files).forEach((f, i) => {
if (f !== file) {
dataTransfer.items.add(f);
dataTransfer.items.add(new File([f], sanitizeFileName(f.name), { type: f.type }));
}
});
input.files = dataTransfer.files;
@ -197,7 +197,7 @@ function setupFileInputPreview(inputId, previewId, isDocument = false, fileType
};
// Use createPhotoPreview for images with filename and size
const photoPreview = createPhotoPreview(e.target.result, deleteHandler, file.name, formatFileSize(file.size));
const photoPreview = createPhotoPreview(e.target.result, deleteHandler, sanitizeFileName(file.name), formatFileSize(file.size));
previewItem.appendChild(photoPreview);
};
reader.readAsDataURL(file);
@ -261,7 +261,7 @@ async function handleFileUploads(asset, isEditMode, isSubAsset = false) {
assetCopy.photoPaths = [];
assetCopy.photoInfo = [];
for (const file of photoInput.files) {
const result = await uploadFile(file, 'image', assetCopy.id);
const result = await uploadFile(new File([file], sanitizeFileName(file.name), { type: file.type }), 'image', assetCopy.id);
if (result) {
assetCopy.photoPaths.push(result.path);
assetCopy.photoInfo.push(result.fileInfo);
@ -284,7 +284,7 @@ async function handleFileUploads(asset, isEditMode, isSubAsset = false) {
assetCopy.receiptPaths = [];
assetCopy.receiptInfo = [];
for (const file of receiptInput.files) {
const result = await uploadFile(file, 'receipt', assetCopy.id);
const result = await uploadFile(new File([file], sanitizeFileName(file.name), { type: file.type }), 'receipt', assetCopy.id);
if (result) {
assetCopy.receiptPaths.push(result.path);
assetCopy.receiptInfo.push(result.fileInfo);
@ -307,7 +307,7 @@ async function handleFileUploads(asset, isEditMode, isSubAsset = false) {
assetCopy.manualPaths = [];
assetCopy.manualInfo = [];
for (const file of manualInput.files) {
const result = await uploadFile(file, 'manual', assetCopy.id);
const result = await uploadFile(new File([file], sanitizeFileName(file.name), { type: file.type }), 'manual', assetCopy.id);
if (result) {
assetCopy.manualPaths.push(result.path);
assetCopy.manualInfo.push(result.fileInfo);
@ -385,7 +385,10 @@ function setupDragAndDrop() {
const file = files[0];
// Use the validateFileType utility function
if (validateFileType(file, fileInput.accept)) {
fileInput.files = files;
fileInput.files = new DataTransfer().files;
const dataTransfer = new DataTransfer();
dataTransfer.items.add(new File([file], sanitizeFileName(file.name), { type: file.type }));
fileInput.files = dataTransfer.files;
fileInput.dispatchEvent(new Event('change'));
} else {
alert('Invalid file type. Please upload a supported file.');

View File

@ -36,4 +36,25 @@ export function formatFileSize(bytes) {
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
/**
* Sanitizes a filename to prevent malicious script calls.
* Removes any characters except alphanumerics, dash, underscore, and dot.
* Also strips leading/trailing dots and spaces, and collapses multiple dots.
* @param {string} filename
* @returns {string} sanitized filename
*/
export function sanitizeFileName(filename) {
if (typeof filename !== 'string') return '';
// Remove path separators and collapse multiple dots
let sanitized = filename.replace(/[/\\]+/g, '')
.replace(/\.+/g, '.')
.replace(/[^a-zA-Z0-9._-]/g, '_')
.replace(/^\.+/, '')
.replace(/\s+/g, '_')
.replace(/\.+$/, '');
// Prevent empty filename
if (!sanitized) sanitized = 'file';
return sanitized;
}