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
-1
@@ -25,12 +25,14 @@ describe("Sitzungen", () => {
|
||||
});
|
||||
|
||||
const sitzung = await sitzungLesen(token);
|
||||
expect(sitzung).toEqual({
|
||||
expect(sitzung).toMatchObject({
|
||||
benutzerId: "b1",
|
||||
email: "admin@example.de",
|
||||
name: "Florian",
|
||||
herkunft: "passwort",
|
||||
});
|
||||
// Der Ausstellungszeitpunkt entscheidet, ob ein Widerruf die Sitzung trifft (#024).
|
||||
expect(sitzung?.ausgestelltAm.getTime()).toBeLessThanOrEqual(Date.now());
|
||||
});
|
||||
|
||||
it("weist ein verändertes oder fremd signiertes Token ab", async () => {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { herkunftStimmt } from "@/app/api/auth/anmelden/route";
|
||||
import type { GeleseneSitzung } from "@/lib/sitzung";
|
||||
import { sitzungBewerten } from "@/server/auth/sitzungspflicht";
|
||||
|
||||
const sitzung = (ausgestelltAm: Date): GeleseneSitzung => ({
|
||||
benutzerId: "b1",
|
||||
email: "admin@example.de",
|
||||
name: "Administrator",
|
||||
herkunft: "passwort",
|
||||
ausgestelltAm,
|
||||
});
|
||||
|
||||
const anfrage = (kopfzeilen: Record<string, string>) =>
|
||||
new Request("https://proxledger.example/api/auth/anmelden", { method: "POST", headers: kopfzeilen });
|
||||
|
||||
describe("Sitzungsprüfung", () => {
|
||||
const jetzt = new Date("2026-09-18T12:00:00Z");
|
||||
const aktiv = { aktiv: true, sitzungenUngueltigAb: null };
|
||||
|
||||
it("lässt eine gültige Sitzung eines aktiven Kontos durch", () => {
|
||||
expect(sitzungBewerten(sitzung(jetzt), aktiv)).toEqual({ gueltig: true });
|
||||
});
|
||||
|
||||
it("weist ab, wenn gar keine Sitzung da ist", () => {
|
||||
expect(sitzungBewerten(null, aktiv).gueltig).toBe(false);
|
||||
});
|
||||
|
||||
it("weist ein gesperrtes Konto ab, obwohl das Token noch gilt", () => {
|
||||
const urteil = sitzungBewerten(sitzung(jetzt), { aktiv: false, sitzungenUngueltigAb: null });
|
||||
expect(urteil).toEqual({ gueltig: false, grund: "Dieses Konto ist gesperrt." });
|
||||
});
|
||||
|
||||
it("weist ein gelöschtes Konto ab", () => {
|
||||
expect(sitzungBewerten(sitzung(jetzt), null).gueltig).toBe(false);
|
||||
});
|
||||
|
||||
it("beendet Sitzungen, die vor dem Widerruf ausgestellt wurden", () => {
|
||||
const widerruf = new Date("2026-09-18T13:00:00Z");
|
||||
const urteil = sitzungBewerten(sitzung(jetzt), { aktiv: true, sitzungenUngueltigAb: widerruf });
|
||||
expect(urteil.gueltig).toBe(false);
|
||||
});
|
||||
|
||||
it("lässt Sitzungen gelten, die nach dem Widerruf entstanden sind", () => {
|
||||
const widerruf = new Date("2026-09-18T11:00:00Z");
|
||||
expect(sitzungBewerten(sitzung(jetzt), { aktiv: true, sitzungenUngueltigAb: widerruf })).toEqual({
|
||||
gueltig: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Herkunft der Anmeldung", () => {
|
||||
it("lässt das eigene Formular durch", () => {
|
||||
expect(herkunftStimmt(anfrage({ origin: "https://proxledger.example", host: "proxledger.example" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("weist eine fremde Seite ab", () => {
|
||||
expect(herkunftStimmt(anfrage({ origin: "https://boese.example", host: "proxledger.example" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("weist einen unbrauchbaren Origin ab", () => {
|
||||
expect(herkunftStimmt(anfrage({ origin: "kein-url", host: "proxledger.example" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("lässt Anfragen ohne Origin zu (curl, alte Browser)", () => {
|
||||
expect(herkunftStimmt(anfrage({ host: "proxledger.example" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("erkennt den Host hinter einem Reverse Proxy", () => {
|
||||
expect(
|
||||
herkunftStimmt(anfrage({ origin: "https://dashboard.kunde.de", "x-forwarded-host": "dashboard.kunde.de" })),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user