From 728132e52a2cdf230a0a86a65d57da5f03e454c1 Mon Sep 17 00:00:00 2001 From: Grovy311 Date: Mon, 28 Sep 2026 21:47:30 +0200 Subject: [PATCH] Warn at startup when no user exists and no bootstrap admin is configured Also document that Sec-Fetch-Site is only sent over HTTPS, so plain-HTTP LAN access relies on SameSite=Lax alone. Co-Authored-By: Claude Opus 5.5 --- docs/authentication.md | 1 + src/domain/services/user.service.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index 13a9f72..15729aa 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -53,6 +53,7 @@ Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle. | Passwort-Raten (offline, DB-Diebstahl) | scrypt (N=16384, r=8, p=1), eigenes Salt je Hash | | Nutzernamen ausspähen (Timing) | Unbekannte Namen werden gegen einen festen Dummy-Hash geprüft – immer genau ein scrypt-Lauf; einheitliche Fehlermeldung | | CSRF | SameSite=Lax **plus** `requireSameOriginForWrites`: schreibende Anfragen nur mit `Sec-Fetch-Site: same-origin`. Nötig, weil andere Dienste auf demselben Host (andere Ports) als „same-site“ gelten | +| CSRF über reines HTTP | Browser senden `Sec-Fetch-Site` nur an HTTPS-Adressen. Beim Zugriff per `http://…` im LAN greift deshalb nur SameSite=Lax (schützt gegen fremde Domains, nicht gegen andere Ports desselben Hosts). | | Session-Fixation | Neue Session-ID bei jedem Login und nach jedem Passwortwechsel | | Gestohlenes Cookie | httpOnly; gleitende Laufzeit (`AUTH_SESSION_HOURS`, 12 h) **und** harte Obergrenze (`AUTH_SESSION_MAX_DAYS`, 7 Tage) | | Kompromittiertes Konto | Passwort-Reset und Deaktivieren beenden sofort alle Sessions des Kontos; Passwortwechsel beendet alle anderen | diff --git a/src/domain/services/user.service.ts b/src/domain/services/user.service.ts index 0968d02..1a3c27c 100644 --- a/src/domain/services/user.service.ts +++ b/src/domain/services/user.service.ts @@ -63,9 +63,14 @@ export class UserService { // Warm the dummy hash at startup so even the first unknown-user login // costs one scrypt run, not two. await this.getDummyHash(); - if (!this.authConfig.bootstrap) return; const existingCount = await this.repository.count(); if (existingCount > 0) return; + if (!this.authConfig.bootstrap) { + logger.warn( + "no users exist and AUTH_BOOTSTRAP_USERNAME/AUTH_BOOTSTRAP_PASSWORT are not set - nobody can sign in" + ); + return; + } const passwordHash = await hashPassword(this.authConfig.bootstrap.password); await this.repository.create({ username: this.authConfig.bootstrap.username,