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
+7
View File
@@ -2,6 +2,7 @@
import { revalidatePath } from "next/cache";
import { anhangSpeichern, UploadFehler } from "@/lib/uploads";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
import {
einkaufBezahltSetzen,
kundenrechnungErfassen,
@@ -48,6 +49,7 @@ async function ausfuehren(arbeit: () => Promise<string | void>): Promise<Aktions
}
export async function laufzeitBestaetigenAktion(formular: FormData) {
await sitzungErforderlich();
const id = formular.get("laufzeitId")?.toString();
if (!id) return;
await laufzeitBestaetigen(id);
@@ -55,6 +57,7 @@ export async function laufzeitBestaetigenAktion(formular: FormData) {
}
export async function laufzeitVerwerfenAktion(formular: FormData) {
await sitzungErforderlich();
const id = formular.get("laufzeitId")?.toString();
if (!id) return;
await laufzeitVerwerfen(id);
@@ -63,6 +66,7 @@ export async function laufzeitVerwerfenAktion(formular: FormData) {
/** Häkchen aus der Rechnungstabelle. */
export async function statusUmschalten(formular: FormData): Promise<void> {
await sitzungErforderlich();
const laufzeitId = formular.get("laufzeitId")?.toString();
const feld = formular.get("feld")?.toString();
const wert = formular.get("wert")?.toString() === "true";
@@ -84,6 +88,7 @@ export async function proxmoxRechnungSpeichern(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
await sitzungErforderlich();
const laufzeitId = formular.get("laufzeitId")?.toString();
if (!laufzeitId) return { fehler: "Laufzeit fehlt." };
@@ -121,6 +126,7 @@ export async function kundenrechnungSpeichern(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
await sitzungErforderlich();
const laufzeitId = formular.get("laufzeitId")?.toString();
if (!laufzeitId) return { fehler: "Laufzeit fehlt." };
@@ -151,6 +157,7 @@ export async function laufzeitAnlegenAktion(
_vorher: AktionsErgebnis,
formular: FormData,
): Promise<AktionsErgebnis> {
await sitzungErforderlich();
const hostId = formular.get("hostId")?.toString();
const key = formular.get("key")?.toString().trim();
const start = datum(formular.get("start"));