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}
-
+ ${escapeHtml(item.original_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) => {
@@ -766,7 +813,7 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {