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-Schlüsselpaar für Web Push (wird automatisch generiert)
|
||||||
VAPID_PUBLIC_KEY=
|
VAPID_PUBLIC_KEY=
|
||||||
VAPID_PRIVATE_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` |
|
| `MODEL` | nein | Standard `deepseek-v4-flash` |
|
||||||
| `THINKING` | nein | `enabled` (Standard) oder `disabled` |
|
| `THINKING` | nein | `enabled` (Standard) oder `disabled` |
|
||||||
| `DEEPSEEK_BASE_URL` | nein | Für Tests/Proxies überschreibbar |
|
| `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)
|
## Deployment mit Docker/Podman (empfohlen)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ const config = {
|
|||||||
model: process.env.MODEL || env.MODEL || 'deepseek-v4-flash',
|
model: process.env.MODEL || env.MODEL || 'deepseek-v4-flash',
|
||||||
thinking: process.env.THINKING || env.THINKING || 'enabled',
|
thinking: process.env.THINKING || env.THINKING || 'enabled',
|
||||||
baseUrl: process.env.DEEPSEEK_BASE_URL || env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',
|
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')),
|
sessionSecret: ensure('SESSION_SECRET', () => crypto.randomBytes(32).toString('hex')),
|
||||||
vapidPublicKey: vapid.publicKey,
|
vapidPublicKey: vapid.publicKey,
|
||||||
vapidPrivateKey: vapid.privateKey,
|
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();
|
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) => {
|
app.post('/api/login', (req, res) => {
|
||||||
const { name, password } = req.body || {};
|
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 || '');
|
const pw = String(config.appPassword || '');
|
||||||
if (!pw || !password || password.length !== pw.length) {
|
const valid =
|
||||||
return res.status(401).json({ error: 'Falsches Passwort' });
|
pw &&
|
||||||
}
|
password &&
|
||||||
const a = crypto.createHash('sha256').update(password).digest();
|
password.length === pw.length &&
|
||||||
const b = crypto.createHash('sha256').update(pw).digest();
|
crypto.timingSafeEqual(
|
||||||
if (!crypto.timingSafeEqual(a, b)) {
|
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' });
|
return res.status(401).json({ error: 'Falsches Passwort' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerLoginOk(ip);
|
||||||
const safeName = String(name || 'Nutzer').slice(0, 40);
|
const safeName = String(name || 'Nutzer').slice(0, 40);
|
||||||
const token = signToken({ n: safeName, exp: Date.now() + 30 * 24 * 3600 * 1000 });
|
const token = signToken({ n: safeName, exp: Date.now() + 30 * 24 * 3600 * 1000 });
|
||||||
res.cookie('research_auth', token, cookieOptions(req));
|
res.cookie('research_auth', token, cookieOptions(req));
|
||||||
|
|||||||
Reference in new issue
Block a user