feat(security): Haertung nach OWASP Top 10 (#024)
Drei echte Luecken gefunden und geschlossen: 1. Die 27 Server Actions verliessen sich allein auf proxy.ts. Jede prueft die Sitzung jetzt selbst und schlaegt gegen die Datenbank nach, ob das Konto noch gilt - damit lassen sich Sitzungen ueberhaupt erst widerrufen. 2. Der Anmelde-Endpunkt prueft die Herkunft des Formulars; vorher war Login-CSRF moeglich. 3. Content-Security-Policy mit Nonce je Anfrage und strict-dynamic. Dazu: npm audit ist ohne Befund (mysql2 und deepmerge-ts per overrides angehoben statt Prisma zurueckzustufen), eine Benutzerverwaltung fuer die Kommandozeile und docs/sicherheit.md mit allem, was bewusst offen bleibt - keine Rollen, keine Zwei-Faktor-Anmeldung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import "dotenv/config";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
|
||||
/**
|
||||
* Benutzerverwaltung von der Kommandozeile – solange es dafür keine Oberfläche gibt.
|
||||
*
|
||||
* npm run benutzer -- liste
|
||||
* npm run benutzer -- sperren <email> Konto sperren und alle Sitzungen sofort beenden
|
||||
* npm run benutzer -- entsperren <email>
|
||||
* npm run benutzer -- passwort <email> neues Passwort setzen, beendet alle Sitzungen
|
||||
*/
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
||||
});
|
||||
|
||||
async function passwortAbfragen(): Promise<string> {
|
||||
const eingabe = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const passwort = await eingabe.question("Neues Passwort (mindestens 12 Zeichen): ");
|
||||
eingabe.close();
|
||||
if (passwort.length < 12) throw new Error("Zu kurz – mindestens 12 Zeichen.");
|
||||
return passwort;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [befehl, email] = process.argv.slice(2);
|
||||
|
||||
if (befehl === "liste") {
|
||||
const alle = await prisma.benutzer.findMany({ orderBy: { email: "asc" } });
|
||||
for (const b of alle) {
|
||||
console.log(`${b.aktiv ? "aktiv " : "gesperrt"} ${b.email.padEnd(32)} ${b.name}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email) throw new Error("Bitte eine E-Mail-Adresse angeben.");
|
||||
const benutzer = await prisma.benutzer.findUnique({ where: { email: email.toLowerCase().trim() } });
|
||||
if (!benutzer) throw new Error(`Kein Benutzer mit der Adresse ${email}.`);
|
||||
|
||||
switch (befehl) {
|
||||
case "sperren":
|
||||
// Sperren allein genügt nicht: Das Sitzungs-Token gilt sonst bis zum Ablauf weiter.
|
||||
await prisma.benutzer.update({
|
||||
where: { id: benutzer.id },
|
||||
data: { aktiv: false, sitzungenUngueltigAb: new Date() },
|
||||
});
|
||||
console.log(`${benutzer.email} ist gesperrt, alle Sitzungen sind beendet.`);
|
||||
break;
|
||||
|
||||
case "entsperren":
|
||||
await prisma.benutzer.update({ where: { id: benutzer.id }, data: { aktiv: true } });
|
||||
console.log(`${benutzer.email} ist wieder freigegeben.`);
|
||||
break;
|
||||
|
||||
case "passwort": {
|
||||
const passwort = await passwortAbfragen();
|
||||
await prisma.benutzer.update({
|
||||
where: { id: benutzer.id },
|
||||
data: {
|
||||
passwortHash: await hash(passwort),
|
||||
sitzungenUngueltigAb: new Date(),
|
||||
},
|
||||
});
|
||||
console.log(`Passwort gesetzt; bestehende Sitzungen von ${benutzer.email} sind beendet.`);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error("Bekannte Befehle: liste, sperren, entsperren, passwort");
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((fehler: unknown) => {
|
||||
console.error(fehler instanceof Error ? fehler.message : fehler);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user