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:
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { protokolliere } from "@/server/audit";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { alertsAbgleichen } from "@/server/alerts/abgleich";
|
||||
import { REGELN } from "@/server/alerts/regeln";
|
||||
import type { AktionsErgebnis } from "./kunden";
|
||||
@@ -15,6 +16,7 @@ function neuLaden() {
|
||||
|
||||
/** Warnung quittieren: sie bleibt offen, ist aber als gesehen markiert. */
|
||||
export async function alertQuittieren(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString();
|
||||
if (!id) return;
|
||||
|
||||
@@ -25,6 +27,7 @@ export async function alertQuittieren(formular: FormData) {
|
||||
|
||||
/** Warnung von Hand schließen; taucht der Fall weiter auf, öffnet der nächste Abgleich sie erneut. */
|
||||
export async function alertSchliessen(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString();
|
||||
if (!id) return;
|
||||
|
||||
@@ -35,6 +38,7 @@ export async function alertSchliessen(formular: FormData) {
|
||||
|
||||
/** Auswertung sofort anstoßen, ohne auf den Worker zu warten. */
|
||||
export async function alertsJetztPruefen(): Promise<AktionsErgebnis> {
|
||||
await sitzungErforderlich();
|
||||
const ergebnis = await alertsAbgleichen();
|
||||
neuLaden();
|
||||
return {
|
||||
@@ -47,6 +51,7 @@ export async function warnregelnSpeichern(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
await sitzungErforderlich();
|
||||
for (const regel of REGELN) {
|
||||
const aktiv = formular.get(`aktiv:${regel.name}`) === "on";
|
||||
const schwellwerte: Record<string, number | number[]> = {};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { verschluesseln } from "@/lib/geheimnisse";
|
||||
import { protokolliere } from "@/server/audit";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import type { EmailKonfiguration, WebhookKonfiguration } from "@/server/notifications/kanaele";
|
||||
import { testNachricht } from "@/server/notifications/nachrichten";
|
||||
import { ueberEinenKanalSenden, zusammenfassungSenden } from "@/server/notifications/versand";
|
||||
@@ -22,6 +23,7 @@ export async function emailKanalSpeichern(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString() || null;
|
||||
const host = formular.get("host")?.toString().trim() ?? "";
|
||||
const port = Number(formular.get("port") ?? 587);
|
||||
@@ -78,6 +80,7 @@ export async function webhookKanalSpeichern(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString() || null;
|
||||
const url = formular.get("url")?.toString().trim() ?? "";
|
||||
|
||||
@@ -116,6 +119,7 @@ export async function webhookKanalSpeichern(
|
||||
}
|
||||
|
||||
export async function kanalLoeschen(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString();
|
||||
if (!id) return;
|
||||
|
||||
@@ -126,6 +130,7 @@ export async function kanalLoeschen(formular: FormData) {
|
||||
|
||||
/** Verschickt eine Testnachricht über einen Kanal. */
|
||||
export async function testSenden(formular: FormData): Promise<void> {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString();
|
||||
if (!id) return;
|
||||
|
||||
@@ -140,6 +145,7 @@ export async function testSenden(formular: FormData): Promise<void> {
|
||||
|
||||
/** Zeitraum der Zusammenfassung speichern (täglich, wöchentlich oder aus). */
|
||||
export async function zusammenfassungSpeichern(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const zeitraum = formular.get("zeitraum")?.toString() ?? "aus";
|
||||
|
||||
await prisma.einstellung.upsert({
|
||||
@@ -154,6 +160,7 @@ export async function zusammenfassungSpeichern(formular: FormData) {
|
||||
|
||||
/** Zusammenfassung sofort verschicken – praktisch zum Ausprobieren. */
|
||||
export async function zusammenfassungJetztSenden(formular: FormData): Promise<void> {
|
||||
await sitzungErforderlich();
|
||||
const zeitraum = formular.get("zeitraum")?.toString() === "woechentlich" ? "woechentlich" : "taeglich";
|
||||
await zusammenfassungSenden(zeitraum);
|
||||
neuLaden();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { protokolliere } from "@/server/audit";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import type { AktionsErgebnis } from "./kunden";
|
||||
|
||||
/** Speichert Notizen und Tags eines Hosts. */
|
||||
@@ -10,6 +11,7 @@ export async function hostNotizenSpeichern(
|
||||
_vorher: AktionsErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<AktionsErgebnis> {
|
||||
await sitzungErforderlich();
|
||||
const hostId = formular.get("hostId")?.toString();
|
||||
if (!hostId) return { fehler: "Host nicht gefunden." };
|
||||
|
||||
@@ -39,6 +41,7 @@ export async function hostNotizenSpeichern(
|
||||
|
||||
/** Ordnet einen einzelnen Host einem Kunden zu (aus der Detailansicht heraus). */
|
||||
export async function hostKundeSetzen(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const hostId = formular.get("hostId")?.toString();
|
||||
const kundeId = formular.get("kundeId")?.toString() || null;
|
||||
const standortId = formular.get("standortId")?.toString() || null;
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { protokolliere } from "@/server/audit";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { hostsZuweisenService, ZuweisungsFehler } from "@/server/services/zuweisung";
|
||||
import type { AktionsErgebnis } from "./kunden";
|
||||
|
||||
/** Weist mehrere Hosts in einem Schritt einem Kunden zu. */
|
||||
export async function hostsZuweisen(_vorher: AktionsErgebnis, formular: FormData): Promise<AktionsErgebnis> {
|
||||
await sitzungErforderlich();
|
||||
const hostIds = formular.getAll("hostId").map(String).filter(Boolean);
|
||||
const kundeId = formular.get("kundeId")?.toString() || null;
|
||||
const standortId = formular.get("standortId")?.toString() || null;
|
||||
@@ -30,6 +32,7 @@ export async function hostsZuweisen(_vorher: AktionsErgebnis, formular: FormData
|
||||
|
||||
/** Hebt die Zuordnung eines einzelnen Hosts auf. */
|
||||
export async function hostZuordnungAufheben(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const hostId = formular.get("hostId")?.toString();
|
||||
if (!hostId) return;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { erzeugeToken } from "@/lib/tokens";
|
||||
import { protokolliere } from "@/server/audit";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import type { AktionsErgebnis } from "./kunden";
|
||||
|
||||
export interface TokenErgebnis extends AktionsErgebnis {
|
||||
@@ -16,6 +17,7 @@ export async function enrollmentTokenErzeugen(
|
||||
_vorher: TokenErgebnis,
|
||||
formular: FormData,
|
||||
): Promise<TokenErgebnis> {
|
||||
await sitzungErforderlich();
|
||||
const bezeichnung = formular.get("bezeichnung")?.toString().trim() || null;
|
||||
const kundeId = formular.get("kundeId")?.toString() || null;
|
||||
const standortId = formular.get("standortId")?.toString() || null;
|
||||
@@ -58,6 +60,7 @@ export async function enrollmentTokenErzeugen(
|
||||
}
|
||||
|
||||
export async function enrollmentTokenWiderrufen(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString();
|
||||
if (!id) return;
|
||||
|
||||
@@ -67,6 +70,7 @@ export async function enrollmentTokenWiderrufen(formular: FormData) {
|
||||
}
|
||||
|
||||
export async function hostTokenWiderrufen(formular: FormData) {
|
||||
await sitzungErforderlich();
|
||||
const id = formular.get("id")?.toString();
|
||||
const hostId = formular.get("hostId")?.toString();
|
||||
if (!id) return;
|
||||
|
||||
Reference in New Issue
Block a user