Login-Rate-Limit: 5 Fehlversuche pro IP → 5 Min Sperre (konfigurierbar)

This commit is contained in:
nico committed 2026-08-26 17:26:41 +02:00
1 parent fe923e650e
commit 5d77fa6f26
5 files changed
+1082 -6

No files matched your search

+58 -6
View File
@@ -68,17 +68,69 @@ app.use((req, res, next) => {
next();
});
// ---------------------------------------------------------------------------
// Login-Rate-Limit: X Fehlversuche pro IP → Y Minuten Sperre
// ---------------------------------------------------------------------------
const loginAttempts = new Map(); // ip -> { count, lockedUntil }
function checkLoginLock(ip) {
const entry = loginAttempts.get(ip);
if (entry && entry.lockedUntil > Date.now()) {
return Math.ceil((entry.lockedUntil - Date.now()) / 1000);
}
return 0;
}
function registerLoginFail(ip) {
const entry = loginAttempts.get(ip) || { count: 0, lockedUntil: 0 };
entry.count += 1;
if (entry.count >= config.loginMaxAttempts) {
entry.lockedUntil = Date.now() + config.loginLockMinutes * 60 * 1000;
entry.count = 0;
}
loginAttempts.set(ip, entry);
}
function registerLoginOk(ip) {
loginAttempts.delete(ip);
}
// Alte Einträge aufräumen (1×/Stunde)
setInterval(() => {
const now = Date.now();
for (const [ip, entry] of loginAttempts) {
if (entry.lockedUntil <= now && entry.count === 0) loginAttempts.delete(ip);
}
}, 60 * 60 * 1000).unref();
app.post('/api/login', (req, res) => {
const { name, password } = req.body || {};
const ip = req.ip;
const lockSeconds = checkLoginLock(ip);
if (lockSeconds > 0) {
return res
.status(429)
.json({ error: `Zu viele Fehlversuche – bitte in ${lockSeconds} Sekunden erneut versuchen.` });
}
const pw = String(config.appPassword || '');
if (!pw || !password || password.length !== pw.length) {
return res.status(401).json({ error: 'Falsches Passwort' });
}
const a = crypto.createHash('sha256').update(password).digest();
const b = crypto.createHash('sha256').update(pw).digest();
if (!crypto.timingSafeEqual(a, b)) {
const valid =
pw &&
password &&
password.length === pw.length &&
crypto.timingSafeEqual(
crypto.createHash('sha256').update(String(password)).digest(),
crypto.createHash('sha256').update(pw).digest()
);
if (!valid) {
registerLoginFail(ip);
return res.status(401).json({ error: 'Falsches Passwort' });
}
registerLoginOk(ip);
const safeName = String(name || 'Nutzer').slice(0, 40);
const token = signToken({ n: safeName, exp: Date.now() + 30 * 24 * 3600 * 1000 });
res.cookie('research_auth', token, cookieOptions(req));