Anmeldung und Sitzungen nach OWASP durchgesehen und gehaertet

Zehn Befunde, alle behoben. Die wichtigsten:

scrypt lief mit N=2^15; das OWASP Password Storage Cheat Sheet nennt 2^17.
Jetzt 2^17, alte Hashes bleiben gueltig und werden beim naechsten Anmelden
still erneuert — das ist der einzige Moment, in dem das Klartextpasswort
vorliegt.

Passwoerter hatten keine Obergrenze. Ein Passwort von mehreren Megabyte haette
scrypt beschaeftigt, und zwar an einem Endpunkt ohne Anmeldung. Jetzt 128
Zeichen, geprueft vor dem Hashen.

Als CSRF-Schutz gab es nur SameSite=Lax. Das Merkblatt sagt ausdruecklich,
dass das allein nicht genuegt — besonders beim Upload, der multipart
entgegennimmt und damit von einem fremden Formular erreichbar waere. Jetzt
wird bei jedem schreibenden Zugriff der Origin gegen den Host der Anfrage
geprueft (nicht gegen BASE_URL, die Anwendung laeuft unter mehreren Domains).

Es gab keinen einzigen Schutzheader — die Verwaltung liess sich einbetten und
per Clickjacking fernsteuern. Jetzt frame-ancestors none, X-Frame-Options,
nosniff, Referrer-Policy und Permissions-Policy; X-Powered-By ist weg.

Sitzungen hatten keine absolute Obergrenze: eine gestohlene Sitzung haette
durch regelmaessige Nutzung unbegrenzt weitergelebt. Jetzt SESSION_MAX_DAYS
neben SESSION_IDLE_DAYS, beide einstellbar.

Dazu: Secure auch hinter einem TLS-Proxy, weniger Auskunft ueber fremde
Konten bei der Anmeldung, eine Bremse je Absender gegen breites
Durchprobieren, Ersteinrichtung ohne Wettlauf, und abgelaufene Sitzungen
werden aufgeraeumt.

Die Datei heisst proxy.ts statt middleware.ts — Next 16 hat die Konvention
umbenannt. /api/uploads bleibt davon ausgenommen: Der Proxy puffert
Anfragekoerper nur bis 10 MB, eine Datei an der Groessengrenze kaeme sonst
unvollstaendig an. Die Herkunftspruefung macht die Route dort selbst.

Befund, Begruendungen und was bewusst offen bleibt: docs/sicherheit.md
This commit is contained in:
Florian Wilde
2026-08-15 08:31:29 +02:00
parent 1064915d37
commit 75252b32b4
17 changed files with 708 additions and 48 deletions
+41
View File
@@ -1,3 +1,44 @@
import { QrRequestError } from "@/lib/qr/type-module";
/**
* Kommt diese Anfrage von der eigenen Oberfläche?
*
* Zweite Verteidigungslinie neben `SameSite=Lax` am Sitzungs-Cookie. Das
* OWASP-Merkblatt zu CSRF sagt ausdrücklich, dass `SameSite` allein nicht
* genügt — es wirkt auf der Ebene der registrierbaren Domain, nicht des
* Ursprungs.
*
* Verglichen wird gegen den Host der Anfrage, nicht gegen `BASE_URL`: Die
* Anwendung ist absichtlich unter mehreren Domains erreichbar.
*
* Fehlt der `Origin`-Header, gilt die Anfrage als eigen: Browser setzen ihn
* bei fremden Zugriffen immer, Skripte und Kommandozeilenwerkzeuge oft gar
* nicht. Ein Abweisen träfe nur die Falschen.
*/
export function isSameOrigin(headers: Headers): boolean {
const origin = headers.get("origin");
if (!origin) return true;
let herkunft: URL;
try {
herkunft = new URL(origin);
} catch {
return false;
}
const host = headers.get("x-forwarded-host") ?? headers.get("host");
return Boolean(host) && herkunft.host === host;
}
export function assertSameOrigin(headers: Headers): void {
if (!isSameOrigin(headers)) {
throw new QrRequestError(
"Diese Anfrage kam von einer fremden Seite und wurde abgewiesen.",
403,
);
}
}
export function jsonError(message: string, status = 400): Response {
return Response.json({ error: message }, { status });
}