Härtung: Login-Drosselung je IP/Konto, Mail-Anhangfehler ohne Retry, doppeltes Upload-Audit
- rate-limit: neuer Scope login (20/15 min, LOGIN_RATE_LIMIT_PER_15_MIN) je IP und je Konto; geprüft in verifyIdentityPassword (Login-Seite + Credentials-Provider), gedrosselt verhält sich wie Fehlanmeldung (generisch, konstante Laufzeit) – L10a - mail/worker: MailAttachmentError wie MailNotConfiguredError unrecoverable (fremder Mandant/Prüfsumme/Größe ändern sich nicht durch Warten) – L11 - field/uploads: Dokument-Audit nur noch in storeFile (vorher doppelt) – L10a - Test test-login-rate-limit Gate 65/65 grün; komplette Suite mit RLS_ENFORCED=true 65/65 grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import "dotenv/config";
|
||||
|
||||
/**
|
||||
* Login throttling (rate-limit scope `login`, checked in verifyIdentityPassword):
|
||||
* (1) within the limit a wrong password returns null, the right one succeeds
|
||||
* (2) after exceeding the per-account limit even the correct password returns null
|
||||
* (3) other accounts are unaffected by one account's counter (per-account key)
|
||||
* (4) resetRateLimits restores access (in-memory counter)
|
||||
* Outside a request there is no IP → only the account counter applies (documented behaviour).
|
||||
*/
|
||||
import { prisma } from "../src/server/db";
|
||||
import { verifyIdentityPassword } from "../src/server/auth";
|
||||
import { RATE_LIMITS, resetRateLimits } from "../src/server/rate-limit";
|
||||
import { hashPassword } from "../src/server/password";
|
||||
|
||||
let failures = 0;
|
||||
const ok = (cond: boolean, msg: string) => {
|
||||
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
|
||||
if (!cond) failures++;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const stamp = Date.now();
|
||||
const pw = "Richtig-Passwort-2026!";
|
||||
const emailA = `zz-rl-a-${stamp}@example.test`;
|
||||
const emailB = `zz-rl-b-${stamp}@example.test`;
|
||||
const hash = await hashPassword(pw);
|
||||
const [a, b] = await Promise.all([
|
||||
prisma.identity.create({ data: { email: emailA, passwordHash: hash } }),
|
||||
prisma.identity.create({ data: { email: emailB, passwordHash: hash } }),
|
||||
]);
|
||||
resetRateLimits();
|
||||
try {
|
||||
// (1) wrong then right, far below the limit (lockout after 5 failures is a separate mechanism)
|
||||
ok((await verifyIdentityPassword(emailA, "falsch")) === null, "(1) falsches Passwort → null");
|
||||
await prisma.identity.update({ where: { id: a.id }, data: { failedLogins: 0, lockedUntil: null } });
|
||||
ok((await verifyIdentityPassword(emailA, pw))?.identityId === a.id, "(1) richtiges Passwort → Erfolg");
|
||||
|
||||
// (2) exhaust the per-account limit with successful attempts (no lockout involved)
|
||||
const limit = RATE_LIMITS.login.limit;
|
||||
for (let i = 0; i < limit; i++) await verifyIdentityPassword(emailA, pw);
|
||||
ok((await verifyIdentityPassword(emailA, pw)) === null, `(2) nach ${limit}+ Versuchen: auch richtiges Passwort → null (gedrosselt)`);
|
||||
|
||||
// (3) other account unaffected
|
||||
ok((await verifyIdentityPassword(emailB, pw))?.identityId === b.id, "(3) anderes Konto nicht betroffen");
|
||||
|
||||
// (4) reset
|
||||
resetRateLimits();
|
||||
ok((await verifyIdentityPassword(emailA, pw))?.identityId === a.id, "(4) nach Reset wieder möglich");
|
||||
} finally {
|
||||
await prisma.identity.deleteMany({ where: { id: { in: [a.id, b.id] } } });
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
console.log(failures ? `\n${failures} Fehler` : "\nOK — Login-Drosselung je Konto wirksam.");
|
||||
process.exit(failures ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user