Datenbank bereinigt / Gitea-Integration gefixt

Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-04 00:24:11 +00:00
committet von Server Deploy
Ursprung 395598c2b0
Commit c21be47428
37 geänderte Dateien mit 30993 neuen und 809 gelöschten Zeilen

Datei anzeigen

@ -7,6 +7,7 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const logger = require('../utils/logger');
@ -18,18 +19,54 @@ if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
// Mapping: Dateiendung -> erlaubte MIME-Types
const EXTENSION_TO_MIME = {
// Dokumente
'pdf': ['application/pdf'],
'doc': ['application/msword'],
'docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'xls': ['application/vnd.ms-excel'],
'xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
'ppt': ['application/vnd.ms-powerpoint'],
'pptx': ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],
'odt': ['application/vnd.oasis.opendocument.text'],
'ods': ['application/vnd.oasis.opendocument.spreadsheet'],
'odp': ['application/vnd.oasis.opendocument.presentation'],
'rtf': ['application/rtf', 'text/rtf'],
// Text
'txt': ['text/plain'],
'csv': ['text/csv', 'application/csv', 'text/comma-separated-values'],
'md': ['text/markdown', 'text/x-markdown', 'text/plain'],
'json': ['application/json', 'text/json'],
'xml': ['application/xml', 'text/xml'],
'html': ['text/html'],
'log': ['text/plain'],
// Bilder
'jpg': ['image/jpeg'],
'jpeg': ['image/jpeg'],
'png': ['image/png'],
'gif': ['image/gif'],
'webp': ['image/webp'],
'svg': ['image/svg+xml'],
'bmp': ['image/bmp'],
'ico': ['image/x-icon', 'image/vnd.microsoft.icon'],
// Archive
'zip': ['application/zip', 'application/x-zip-compressed'],
'rar': ['application/x-rar-compressed', 'application/vnd.rar'],
'7z': ['application/x-7z-compressed'],
'tar': ['application/x-tar'],
'gz': ['application/gzip', 'application/x-gzip'],
// Code/Skripte (als text/plain akzeptiert)
'sql': ['application/sql', 'text/plain'],
'js': ['text/javascript', 'application/javascript', 'text/plain'],
'css': ['text/css', 'text/plain'],
'py': ['text/x-python', 'text/plain'],
'sh': ['application/x-sh', 'text/plain']
};
// Standard-Werte (Fallback)
let MAX_FILE_SIZE = (parseInt(process.env.MAX_FILE_SIZE_MB) || 15) * 1024 * 1024;
let ALLOWED_MIME_TYPES = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain', 'text/csv', 'text/markdown',
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
'application/json'
];
let ALLOWED_EXTENSIONS = ['pdf', 'docx', 'txt'];
/**
* Lädt Upload-Einstellungen aus der Datenbank
@ -43,17 +80,9 @@ function loadUploadSettings() {
if (settings) {
MAX_FILE_SIZE = (settings.maxFileSizeMB || 15) * 1024 * 1024;
// Erlaubte MIME-Types aus den aktiven Kategorien zusammenstellen
const types = [];
if (settings.allowedTypes) {
Object.values(settings.allowedTypes).forEach(category => {
if (category.enabled && Array.isArray(category.types)) {
types.push(...category.types);
}
});
}
if (types.length > 0) {
ALLOWED_MIME_TYPES = types;
// Erlaubte Endungen aus den Einstellungen
if (Array.isArray(settings.allowedExtensions) && settings.allowedExtensions.length > 0) {
ALLOWED_EXTENSIONS = settings.allowedExtensions;
}
}
} catch (error) {
@ -67,7 +96,7 @@ function loadUploadSettings() {
*/
function getCurrentSettings() {
loadUploadSettings();
return { maxFileSize: MAX_FILE_SIZE, allowedMimeTypes: ALLOWED_MIME_TYPES };
return { maxFileSize: MAX_FILE_SIZE, allowedExtensions: ALLOWED_EXTENSIONS };
}
/**
@ -99,19 +128,83 @@ const storage = multer.diskStorage({
});
/**
* Datei-Filter
* Gefährliche Dateinamen prüfen
*/
function isSecureFilename(filename) {
// Null-Bytes, Pfad-Traversal, Steuerzeichen blocken
const dangerousPatterns = [
/\x00/, // Null-Bytes
/\.\./, // Path traversal
/[<>:"\\|?*]/, // Windows-spezifische gefährliche Zeichen
/[\x00-\x1F]/, // Steuerzeichen
/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i, // Windows reservierte Namen
];
return !dangerousPatterns.some(pattern => pattern.test(filename));
}
/**
* Datei-Filter: Erweiterte Sicherheitsprüfungen
*/
const fileFilter = (req, file, cb) => {
// Aktuelle Einstellungen laden
const settings = getCurrentSettings();
// MIME-Type prüfen
if (settings.allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
logger.warn(`Abgelehnter Upload: ${file.originalname} (${file.mimetype})`);
cb(new Error(`Dateityp nicht erlaubt: ${file.mimetype}`), false);
// Sicherheitsprüfungen für Dateinamen
if (!isSecureFilename(file.originalname)) {
logger.warn(`Unsicherer Dateiname abgelehnt: ${file.originalname}`);
cb(new Error('Dateiname enthält nicht erlaubte Zeichen'), false);
return;
}
// Dateiname-Länge prüfen
if (file.originalname.length > 255) {
logger.warn(`Dateiname zu lang: ${file.originalname}`);
cb(new Error('Dateiname ist zu lang (max. 255 Zeichen)'), false);
return;
}
// Dateiendung extrahieren (ohne Punkt, lowercase)
const ext = path.extname(file.originalname).toLowerCase().replace('.', '');
// Doppelte Dateiendungen verhindern (z.B. script.txt.exe)
const nameWithoutExt = path.basename(file.originalname, path.extname(file.originalname));
if (path.extname(nameWithoutExt)) {
logger.warn(`Doppelte Dateiendung abgelehnt: ${file.originalname}`);
cb(new Error('Dateien mit mehreren Endungen sind nicht erlaubt'), false);
return;
}
// Prüfen ob Endung erlaubt ist
if (!settings.allowedExtensions.includes(ext)) {
logger.warn(`Abgelehnter Upload (Endung): ${file.originalname} (.${ext})`);
cb(new Error(`Dateityp .${ext} nicht erlaubt`), false);
return;
}
// Executable Dateien zusätzlich blocken
const executableExtensions = [
'exe', 'bat', 'cmd', 'com', 'scr', 'pif', 'vbs', 'vbe', 'js', 'jar',
'app', 'deb', 'pkg', 'dmg', 'run', 'bin', 'msi', 'gadget'
];
if (executableExtensions.includes(ext)) {
logger.warn(`Executable Datei abgelehnt: ${file.originalname}`);
cb(new Error('Ausführbare Dateien sind nicht erlaubt'), false);
return;
}
// MIME-Type gegen bekannte Typen prüfen
const expectedMimes = EXTENSION_TO_MIME[ext];
if (expectedMimes && !expectedMimes.includes(file.mimetype)) {
logger.warn(`MIME-Mismatch: ${file.originalname} (erwartet: ${expectedMimes.join('/')}, bekommen: ${file.mimetype})`);
// Bei kritischen Mismatches ablehnen
if (file.mimetype === 'application/octet-stream' || file.mimetype.startsWith('application/x-')) {
cb(new Error('Verdächtiger Dateityp erkannt'), false);
return;
}
}
cb(null, true);
};
/**
@ -194,5 +287,6 @@ module.exports = {
getCurrentSettings,
UPLOAD_DIR,
MAX_FILE_SIZE,
ALLOWED_MIME_TYPES
ALLOWED_EXTENSIONS,
EXTENSION_TO_MIME
};