mirror of
https://github.com/DumbWareio/DumbAssets.git
synced 2026-01-09 06:10:52 +08:00
Merge pull request #40 from DumbWareio/fix/sanitize-file-names
Add filename sanitization on upload on front and backend
This commit is contained in:
commit
f65bee9453
@ -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',
|
||||
|
||||
28
server.js
28
server.js
@ -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)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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.');
|
||||
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user