Login-Rate-Limit: 5 Fehlversuche pro IP → 5 Min Sperre (konfigurierbar)
This commit is contained in:
1 parent
fe923e650e
commit
5d77fa6f26
5 files changed
+1082
-6
No files matched your search
@@ -17,3 +17,6 @@ THINKING=enabled
|
||||
# VAPID-Schlüsselpaar für Web Push (wird automatisch generiert)
|
||||
VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
# Login-Schutz: Fehlversuche pro IP bevor gesperrt wird, und Sperrdauer
|
||||
LOGIN_MAX_ATTEMPTS=5
|
||||
LOGIN_LOCK_MINUTES=5
|
||||
@@ -37,6 +37,8 @@ erzeugt und in die `.env` geschrieben.
|
||||
| `MODEL` | nein | Standard `deepseek-v4-flash` |
|
||||
| `THINKING` | nein | `enabled` (Standard) oder `disabled` |
|
||||
| `DEEPSEEK_BASE_URL` | nein | Für Tests/Proxies überschreibbar |
|
||||
| `LOGIN_MAX_ATTEMPTS` | nein | Fehlversuche pro IP vor Sperre (Standard 5) |
|
||||
| `LOGIN_LOCK_MINUTES` | nein | Sperrdauer nach Fehlversuchen (Standard 5) |
|
||||
|
||||
## Deployment mit Docker/Podman (empfohlen)
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ const config = {
|
||||
model: process.env.MODEL || env.MODEL || 'deepseek-v4-flash',
|
||||
thinking: process.env.THINKING || env.THINKING || 'enabled',
|
||||
baseUrl: process.env.DEEPSEEK_BASE_URL || env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',
|
||||
loginMaxAttempts: Number(process.env.LOGIN_MAX_ATTEMPTS || env.LOGIN_MAX_ATTEMPTS || 5),
|
||||
loginLockMinutes: Number(process.env.LOGIN_LOCK_MINUTES || env.LOGIN_LOCK_MINUTES || 5),
|
||||
sessionSecret: ensure('SESSION_SECRET', () => crypto.randomBytes(32).toString('hex')),
|
||||
vapidPublicKey: vapid.publicKey,
|
||||
vapidPrivateKey: vapid.privateKey,
|
||||
|
||||
Generated
+1017
File diff suppressed because it is too large.
Load diff
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user