diff --git a/expressjs/src/server.js b/expressjs/src/server.js index 5d14ce4..08975ff 100644 --- a/expressjs/src/server.js +++ b/expressjs/src/server.js @@ -36,6 +36,8 @@ const upload = multer({ }); const app = express(); +const trustProxy = process.env.TRUST_PROXY === 'true'; +app.set('trust proxy', trustProxy); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use(express.json()); @@ -110,6 +112,46 @@ function logEvent(event, owner, detail) { ).catch(() => undefined); } +function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +const loginAttempts = new Map(); +const LOGIN_WINDOW_MS = 15 * 60 * 1000; +const LOGIN_MAX_ATTEMPTS = 10; + +function loginRateLimit(type) { + return (req, res, next) => { + const ip = req.ip || 'unknown'; + const key = `${type}:${ip}`; + const now = Date.now(); + const entry = loginAttempts.get(key) || { count: 0, resetAt: now + LOGIN_WINDOW_MS }; + if (now > entry.resetAt) { + entry.count = 0; + entry.resetAt = now + LOGIN_WINDOW_MS; + } + entry.count += 1; + loginAttempts.set(key, entry); + if (entry.count > LOGIN_MAX_ATTEMPTS) { + const waitMinutes = Math.ceil((entry.resetAt - now) / 60000); + const body = `

Zu viele Anmeldeversuche. Bitte in ${waitMinutes} Minuten erneut versuchen.

`; + res.status(429).send(renderPage('Zu viele Versuche', body)); + return; + } + next(); + }; +} + +function clearLoginAttempts(type, req) { + const ip = req.ip || 'unknown'; + loginAttempts.delete(`${type}:${ip}`); +} + function parseLogins(contents) { const entries = new Map(); const lines = contents.split(/\r?\n/); @@ -440,7 +482,7 @@ app.get(`${basePath}/login`, (req, res) => { res.send(renderPage('Anmeldung', body)); }); -app.post(`${basePath}/login`, async (req, res) => { +app.post(`${basePath}/login`, loginRateLimit('user'), async (req, res) => { const username = String(req.body.username || '').trim(); const password = String(req.body.password || ''); const logins = await loadLogins(); @@ -480,6 +522,7 @@ app.post(`${basePath}/login`, async (req, res) => { maxAge: jwtMaxAgeMs, secure: process.env.COOKIE_SECURE === 'true', }); + clearLoginAttempts('user', req); await logEvent('login', username, { ok: true }); res.redirect(baseUrl('/dashboard')); }); @@ -519,7 +562,7 @@ app.get(`${basePath}/admin`, async (req, res) => { res.send(renderPage('Admin', body)); }); -app.post(`${basePath}/admin/login`, async (req, res) => { +app.post(`${basePath}/admin/login`, loginRateLimit('admin'), async (req, res) => { if (!adminHash) { res.status(404).send(renderPage('Admin', '

Admin-Zugang ist nicht konfiguriert.

')); return; @@ -554,6 +597,7 @@ app.post(`${basePath}/admin/login`, async (req, res) => { maxAge: jwtMaxAgeMs, secure: process.env.COOKIE_SECURE === 'true', }); + clearLoginAttempts('admin', req); await logEvent('admin_login', 'admin', { ok: true }); res.redirect(baseUrl('/admin/dashboard')); }); @@ -598,20 +642,21 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => { const logRows = recentLogs.map((entry) => ` ${formatTimestamp(entry.created_at)} - ${entry.event} - ${entry.owner || '—'} - ${entry.detail || ''} + ${escapeHtml(entry.event)} + ${escapeHtml(entry.owner || '—')} + ${escapeHtml(entry.detail || '')} `).join(''); const adminUploadsRows = allUploads.map((item) => { const fileUrl = `/_share/${item.stored_name}`; + const fileHref = encodeURI(fileUrl); return ` - ${item.owner} + ${escapeHtml(item.owner)} -
${item.original_name}
-
${item.stored_name}
+
${escapeHtml(item.original_name)}
+
${escapeHtml(item.stored_name)}
${formatBytes(item.size_bytes)} @@ -712,23 +757,25 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => { const rowForEntry = (entry, isDir) => { const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`); + const escapedName = escapeHtml(entry.name); + const escapedPath = escapeHtml(childPath); return ` ${isDir ? '[DIR]' : '[FILE]'} - ${isDir ? `${entry.name}` : entry.name} + ${isDir ? `${escapedName}` : escapedName} ${isDir ? 'Ordner' : 'Datei'}
- +
- +
@@ -757,7 +804,7 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
- Pfad: /${relativePath || ''} + Pfad: /${escapeHtml(relativePath || '')} ${relativePath ? `← Zurück` : ''}
@@ -766,7 +813,7 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {

Ordner erstellen

- +