DEV1.0: Initial commit - Workflow Portal with security fixes
- Backend: Express.js + PostgreSQL/SQLite with LDAP/AD integration - Frontend: React 18 + Vite + TailwindCSS/DaisyUI - Security fixes applied (2026-07 + 2026-08): - LDAP injection prevention, CSRF protection, HttpOnly cookies - Session hashing (SHA-256), account lockout, rate limiting - Input validation (zod), file upload security, CSP/HSTS headers - V3: express-rate-limit updated (ip-address SSRF fix) - V4: postcss updated (nanoid DoS fix) - V5: Rate-limit on /health endpoint - V6: Session rotation on login (session fixation prevention) - V9: Task values array limit (DoS prevention) - V10: Frontend XSS audit completed - Docker: Multi-stage build, non-root user, PostgreSQL + backup service
This commit is contained in:
65
backend/routes/upload.js
Normal file
65
backend/routes/upload.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* File upload routes module.
|
||||
*/
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const multer = require('multer');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { uploadLimiter } = require('../middleware/rateLimit');
|
||||
const { auditLog } = require('../auditLog');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// File upload setup
|
||||
const uploadDir = path.join(__dirname, '..', 'data', 'uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// VULN-08/09: Secure file upload
|
||||
const ALLOWED_MIMES = ['application/pdf', 'image/png', 'image/jpeg', 'image/gif', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||
const ALLOWED_EXTS = ['.pdf', '.png', '.jpg', '.jpeg', '.gif', '.txt', '.doc', '.docx'];
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, uploadDir),
|
||||
filename: (req, file, cb) => {
|
||||
const safeName = path.basename(file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const ext = path.extname(safeName).toLowerCase();
|
||||
const safeExt = ALLOWED_EXTS.includes(ext) ? ext : '.bin';
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
cb(null, uniqueSuffix + '-' + safeName.replace(/\.[^.]+$/, '') + safeExt);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (ALLOWED_MIMES.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Dateityp nicht erlaubt. Erlaubt: PDF, PNG, JPG, GIF, TXT, DOC, DOCX.'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// P12: Serve uploads as attachments (prevent XSS) - requires authentication
|
||||
router.use('/uploads', authMiddleware, express.static(uploadDir, {
|
||||
setHeaders: (res) => {
|
||||
res.setHeader('Content-Disposition', 'attachment');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
},
|
||||
}));
|
||||
|
||||
// Upload endpoint - Punkt 23: Rate limited per user
|
||||
router.post('/', authMiddleware, uploadLimiter, upload.single('file'), (req, res) => {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Keine Datei hochgeladen.' });
|
||||
}
|
||||
const fileUrl = '/uploads/' + req.file.filename;
|
||||
auditLog(req.user?.id, 'file_upload', null, null, `Uploaded: ${req.file.filename} (${req.file.size} bytes)`);
|
||||
res.json({ filename: req.file.filename, originalname: req.file.originalname, url: fileUrl, size: req.file.size });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user