mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
Security hardening, backup restore fix & restore warning modal
- Fix backup restore: try/finally ensures DB always reopens after closeDb - Fix EBUSY on uploads during restore (in-place overwrite instead of rmSync) - Add DB proxy null guard for clearer errors during restore window - Add red warning modal before backup restore (DE/EN, dark mode support) - JWT secret: empty docker-compose default so auto-generation kicks in - OIDC: pass token via URL fragment instead of query param (no server logs) - Block SVG uploads on photos, files and covers (stored XSS prevention) - Add helmet for security headers (HSTS, X-Frame, nosniff, etc.) - Explicit express.json body size limit (100kb) - Fix XSS in Leaflet map markers (escape image_url in HTML) - Remove verbose WebSocket debug logging from client
This commit is contained in:
+26
-14
@@ -138,25 +138,37 @@ async function restoreFromZip(zipPath, res) {
|
||||
// Step 1: close DB connection BEFORE touching the file (required on Windows)
|
||||
closeDb();
|
||||
|
||||
// Step 2: remove WAL/SHM and overwrite DB file
|
||||
const dbDest = path.join(dataDir, 'travel.db');
|
||||
for (const ext of ['', '-wal', '-shm']) {
|
||||
try { fs.unlinkSync(dbDest + ext); } catch (e) {}
|
||||
}
|
||||
fs.copyFileSync(extractedDb, dbDest);
|
||||
try {
|
||||
// Step 2: remove WAL/SHM and overwrite DB file
|
||||
const dbDest = path.join(dataDir, 'travel.db');
|
||||
for (const ext of ['', '-wal', '-shm']) {
|
||||
try { fs.unlinkSync(dbDest + ext); } catch (e) {}
|
||||
}
|
||||
fs.copyFileSync(extractedDb, dbDest);
|
||||
|
||||
// Step 3: restore uploads
|
||||
const extractedUploads = path.join(extractDir, 'uploads');
|
||||
if (fs.existsSync(extractedUploads)) {
|
||||
if (fs.existsSync(uploadsDir)) fs.rmSync(uploadsDir, { recursive: true, force: true });
|
||||
fs.cpSync(extractedUploads, uploadsDir, { recursive: true });
|
||||
// Step 3: restore uploads — overwrite in-place instead of rmSync
|
||||
// (rmSync fails with EBUSY because express.static holds the directory)
|
||||
const extractedUploads = path.join(extractDir, 'uploads');
|
||||
if (fs.existsSync(extractedUploads)) {
|
||||
// Clear contents of each subdirectory without removing the root uploads dir
|
||||
for (const sub of fs.readdirSync(uploadsDir)) {
|
||||
const subPath = path.join(uploadsDir, sub);
|
||||
if (fs.statSync(subPath).isDirectory()) {
|
||||
for (const file of fs.readdirSync(subPath)) {
|
||||
try { fs.unlinkSync(path.join(subPath, file)); } catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy restored files over
|
||||
fs.cpSync(extractedUploads, uploadsDir, { recursive: true, force: true });
|
||||
}
|
||||
} finally {
|
||||
// Step 4: ALWAYS reopen DB — even if file copy failed, so the server stays functional
|
||||
reinitialize();
|
||||
}
|
||||
|
||||
fs.rmSync(extractDir, { recursive: true, force: true });
|
||||
|
||||
// Step 4: reopen DB with restored data
|
||||
reinitialize();
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Restore error:', err);
|
||||
|
||||
@@ -35,6 +35,11 @@ const upload = multer({
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
];
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const blockedExts = ['.svg', '.html', '.htm', '.xml'];
|
||||
if (blockedExts.includes(ext) || file.mimetype.includes('svg')) {
|
||||
return cb(new Error('File type not allowed'));
|
||||
}
|
||||
if (allowed.includes(file.mimetype) || file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
|
||||
@@ -196,7 +196,7 @@ router.get('/callback', async (req, res) => {
|
||||
// Generate JWT and redirect to frontend
|
||||
const token = generateToken(user);
|
||||
// In dev mode, frontend runs on a different port
|
||||
res.redirect(frontendUrl(`/login?token=${token}`));
|
||||
res.redirect(frontendUrl(`/login#token=${token}`));
|
||||
} catch (err) {
|
||||
console.error('[OIDC] Callback error:', err);
|
||||
res.redirect(frontendUrl('/login?oidc_error=server_error'));
|
||||
|
||||
@@ -25,10 +25,12 @@ const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
if (file.mimetype.startsWith('image/') && !file.mimetype.includes('svg') && allowedExts.includes(ext)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Nur Bilddateien sind erlaubt'));
|
||||
cb(new Error('Only jpg, png, gif, webp images allowed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,8 +24,13 @@ const uploadCover = multer({
|
||||
storage: coverStorage,
|
||||
limits: { fileSize: 20 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) cb(null, true);
|
||||
else cb(new Error('Nur Bilder erlaubt'));
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
if (file.mimetype.startsWith('image/') && !file.mimetype.includes('svg') && allowedExts.includes(ext)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only jpg, png, gif, webp images allowed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user