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:
2026-09-18 23:45:35 +02:00
co-authored by Claude Opus 5
parent 94f1eb4e6f
commit 7775b6211f
35 changed files with 525 additions and 48 deletions
+5
View File
@@ -5,6 +5,7 @@ import { redirect } from "next/navigation";
import { z } from "zod";
import { prisma } from "@/lib/db";
import { protokolliere } from "@/server/audit";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
export interface AktionsErgebnis {
fehler?: string;
@@ -28,6 +29,7 @@ const kundeSchema = z.object({
/** Legt einen Kunden an oder ändert ihn; `id` im Formular entscheidet darüber. */
export async function kundeSpeichern(_vorher: AktionsErgebnis, formular: FormData): Promise<AktionsErgebnis> {
await sitzungErforderlich();
const id = leerZuNull(formular.get("id"));
const eingabe = kundeSchema.safeParse({
name: formular.get("name")?.toString().trim() ?? "",
@@ -66,6 +68,7 @@ export async function kundeSpeichern(_vorher: AktionsErgebnis, formular: FormDat
}
export async function kundeLoeschen(formular: FormData) {
await sitzungErforderlich();
const id = formular.get("id")?.toString();
if (!id) return;
@@ -91,6 +94,7 @@ export async function standortSpeichern(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
await sitzungErforderlich();
const id = leerZuNull(formular.get("id"));
const eingabe = standortSchema.safeParse({
kundeId: formular.get("kundeId")?.toString() ?? "",
@@ -116,6 +120,7 @@ export async function standortSpeichern(
}
export async function standortLoeschen(formular: FormData) {
await sitzungErforderlich();
const id = formular.get("id")?.toString();
const kundeId = formular.get("kundeId")?.toString();
if (!id) return;