Files
ProxLedger/tests/sicherheit.test.ts
T
florian.wildeandClaude Opus 5 94d5edbc6c fix(adresse): Weiterleitungen nie aus der Lausch-Adresse bauen (#033, #032)
Abmelden fuehrte im Docker-Betrieb auf 0.0.0.0:3000 - eine Adresse, die kein
Browser aufrufen kann. Ursache war hausgemacht: ENV HOSTNAME=0.0.0.0 aus
#031 reparierte die Zustandspruefung und lenkte zugleich alle Weiterleitungen
aus Server Actions dorthin, weil Next sie serverseitig absolut macht. Vorher
stand dort der Containername - derselbe Fehler, nur unauffaelliger.

Neuer Helfer eigenerPfad() baut die Adresse aus Einstellung, APP_URL oder den
Kopfzeilen der Anfrage. Betroffen waren Abmelden, die Sitzungspruefung und das
Loeschen eines Kunden. Zusaetzlich uebergeht oeffentlicheBasis() eine als
Lausch-Adresse eingetragene APP_URL, statt sie zu uebernehmen.

Gegen einen Docker-Stapel geprueft: Mit APP_URL=http://0.0.0.0:3000 leitet die
Anwendung jetzt auf den tatsaechlich aufgerufenen Namen.

Dazu #032: Das ct-Skript setzt die Herkunft der App-Skripte selbst, und die
Speichervorgabe steht auf 4 GB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 15:35:19 +02:00

121 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { herkunftStimmt } from "@/app/api/auth/anmelden/route";
import { eigeneAdresse } from "@/lib/adresse";
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);
});
});
describe("Öffentliche Adresse", () => {
// APP_URL kommt sonst aus der .env der Entwicklungsumgebung und verfälscht die Rückfallstufen.
beforeEach(() => {
vi.stubEnv("APP_URL", "");
});
afterEach(() => {
vi.unstubAllEnvs();
});
const anfrageMit = (kopfzeilen: Record<string, string>) =>
new Request("http://0a5963a591df:3000/api/auth/anmelden", { method: "POST", headers: kopfzeilen });
it("nimmt die eingetragene Adresse, nicht die interne des Containers", () => {
const adresse = eigeneAdresse(anfrageMit({}), "/hosts", "https://proxledger.example.de");
expect(adresse.toString()).toBe("https://proxledger.example.de/hosts");
});
it("fällt auf die Kopfzeilen des Reverse Proxy zurück", () => {
const anfrage = anfrageMit({ "x-forwarded-host": "dashboard.kunde.de", "x-forwarded-proto": "https" });
expect(eigeneAdresse(anfrage, "/").toString()).toBe("https://dashboard.kunde.de/");
});
it("nimmt notfalls die Anfrage selbst", () => {
expect(eigeneAdresse(anfrageMit({}), "/").host).toBe("0a5963a591df:3000");
});
it("übergeht eine Lausch-Adresse in der Konfiguration", () => {
// 0.0.0.0 heißt „alle Schnittstellen“ – als Ziel im Browser ist das unbrauchbar.
// Genau so stand es nach einer Installation in der .env und führte beim Abmelden ins Leere.
const anfrage = anfrageMit({ host: "proxledger.firma.de" });
expect(eigeneAdresse(anfrage, "/login", "http://0.0.0.0:3000").host).toBe("proxledger.firma.de");
});
it("übergeht auch eine Lausch-Adresse aus der Umgebung", () => {
vi.stubEnv("APP_URL", "http://0.0.0.0:3000");
const anfrage = anfrageMit({ host: "proxledger.firma.de" });
expect(eigeneAdresse(anfrage, "/login").host).toBe("proxledger.firma.de");
});
it("übergeht einen unbrauchbaren Eintrag, statt zu scheitern", () => {
const anfrage = anfrageMit({ "x-forwarded-host": "dashboard.kunde.de" });
expect(eigeneAdresse(anfrage, "/", "keine-adresse").host).toBe("dashboard.kunde.de");
});
});