Befunde der Sicherheitspruefung abarbeiten
S1 (hoch): Die OIDC-Rueckkehr verknuepft eine Identitaet mit einem bestehenden Konto nur bei bestaetigter E-Mail — geprueft wurde das auch, nur war die Bestaetigung als `email_verified !== false` berechnet. Damit galt auch ein fehlender Claim als bestaetigt, und genau den schicken manche Anbieter gar nicht. Jetzt zaehlt allein ein ausdrueckliches `true`. S2 (hoch): `isAuthEnabled()` verlangte den Wert "true"; alles andere hiess "aus". Eine vergessene Variable ergab eine Anwendung, in der jeder Besucher alles darf, und nichts sah danach aus. Umgedreht: Abgeschaltet wird nur, wer ausdruecklich "false" setzt. Die Vorgaben in Compose und .env.example stehen auf true, und der Container warnt beim Start mehrzeilig, solange die Anmeldung aus ist. S5 (mittel): Der OIDC-Bootstrap las `user.count()` und legte danach an — zwei gleichzeitige erste Anmeldungen konnten beide Administrator werden. Jetzt in einer serialisierbaren Transaktion, wie die lokale Ersteinrichtung es laengst tut. S6 (niedrig): Die Herkunftspruefung verglich nur den Host, nicht das Schema. Das Schema kommt jetzt aus BASE_URL — die eine Angabe zum Ursprung, ueber die der Aufrufer nicht mitbestimmt. S3, S4 und S7 bleiben offen und stehen begruendet in docs/sicherheit.md. Bei S3 wurde der naheliegende Ausweg geprueft und verworfen: Waehrend der Sperre weiter Passwoerter zu pruefen kostet 128 MiB je Versuch und tauschte eine Sperre gegen ein Konto gegen eine gegen alle. Refs #20
This commit is contained in:
+112
-1
@@ -1,7 +1,9 @@
|
||||
# Anmeldung und Sitzungen — Durchsicht nach OWASP
|
||||
|
||||
Durchsicht der Anmeldung, der Sitzungsverwaltung und der Berechtigungen gegen
|
||||
die einschlägigen OWASP-Merkblätter. Stand: 15.08.2026.
|
||||
die einschlägigen OWASP-Merkblätter. Stand: 15.08.2026, ergänzt am 24.08.2026
|
||||
um die Befunde einer externen Codeprüfung (Punkte 11 bis 14 und die drei
|
||||
offenen Punkte am Ende).
|
||||
|
||||
Geprüft gegen:
|
||||
|
||||
@@ -163,6 +165,72 @@ Nutzung abgewiesen —, aber die Tabelle wuchs unbegrenzt.
|
||||
**Behoben:** Beim Nachführen einer Sitzung (höchstens stündlich je Sitzung)
|
||||
wird beiläufig aufgeräumt.
|
||||
|
||||
### 11. OIDC wertete eine unbestätigte E-Mail als bestätigt
|
||||
|
||||
**Befund:** Der schwerste der externen Prüfung. Findet die OIDC-Rückkehr ein
|
||||
lokales Konto mit derselben E-Mail-Adresse, hängt sie die Identität des
|
||||
Anbieters daran — aber nur, wenn die Adresse **bestätigt** ist. Genau das
|
||||
prüfte der Code auch, nur war die Bestätigung falsch berechnet:
|
||||
|
||||
```ts
|
||||
emailVerified: payload.email_verified !== false
|
||||
```
|
||||
|
||||
Damit galt nicht nur `true` als bestätigt, sondern auch ein **fehlender
|
||||
Claim**, `null`, `0` und jede Zeichenkette. Ein Anbieter, der
|
||||
`email_verified` gar nicht mitschickt — das ist erlaubt und kommt vor —,
|
||||
hätte die Verknüpfung geöffnet. Wer sich dort eine fremde Adresse eintragen
|
||||
kann, hätte damit das hiesige Konto übernommen, im schlimmsten Fall das eines
|
||||
Administrators.
|
||||
|
||||
**Behoben:** Nur ein ausdrückliches `true` zählt. Alles andere führt zur
|
||||
bisherigen Meldung „der Anbieter hat sie nicht bestätigt“, und die
|
||||
Verknüpfung unterbleibt.
|
||||
|
||||
**Was das nicht war:** kein Angriff ohne Zugriff auf den Anbieter — das
|
||||
ID-Token muss weiterhin gültig signiert sein und Aussteller, Empfänger und
|
||||
Nonce bestehen.
|
||||
|
||||
### 12. Eine fehlende Angabe schaltete die Anmeldung ab
|
||||
|
||||
**Befund:** `isAuthEnabled()` verlangte den Wert `true`; alles andere hieß
|
||||
„aus“. Eine vergessene oder falsch geschriebene Variable ergab damit eine
|
||||
Anwendung, in der jeder Besucher alles darf — und nichts sah danach aus. Die
|
||||
Compose-Vorgabe stand zusätzlich auf `false`.
|
||||
|
||||
**Behoben:** Umgedreht. Abgeschaltet wird nur, wer ausdrücklich `false`
|
||||
setzt; ein fehlender Wert bedeutet „an“. Die Vorgaben in `docker-compose.yml`
|
||||
und `.env.example` stehen auf `true`. Steht die Anmeldung aus, schreibt der
|
||||
Container beim Start eine mehrzeilige Warnung ins Protokoll.
|
||||
|
||||
Der offene Betrieb bleibt möglich — er ist für Einzelplatz und LAN gedacht
|
||||
und dokumentiert. Er ist nur nicht mehr das, was man versehentlich bekommt.
|
||||
|
||||
### 13. Wettlauf beim ersten Administrator über OIDC
|
||||
|
||||
**Befund:** Der OIDC-Pfad las `user.count()` und legte danach an. Zwei
|
||||
gleichzeitige erste Anmeldungen konnten sich beide für die erste halten und
|
||||
beide als Instanz-Administrator entstehen. Die Passwort-Ersteinrichtung
|
||||
sicherte denselben Ablauf längst mit einer serialisierbaren Transaktion.
|
||||
|
||||
**Behoben:** Derselbe Schutz auch hier — Zählen und Anlegen liegen in einer
|
||||
Transaktion mit `isolationLevel: "Serializable"`.
|
||||
|
||||
### 14. Die Herkunftsprüfung verglich das Schema nicht
|
||||
|
||||
**Befund:** `isSameOrigin()` verglich nur den Host. `http://example.com` galt
|
||||
damit als eigen, obwohl die Anwendung unter `https://example.com` läuft.
|
||||
|
||||
**Behoben:** Das Schema wird mitverglichen, und es kommt aus `BASE_URL` — der
|
||||
einen Angabe zum Ursprung, über die der Aufrufer nicht mitbestimmt.
|
||||
|
||||
**Zur Einordnung:** Die Prüfung vergleicht weiterhin gegen den `Host`- bzw.
|
||||
`X-Forwarded-Host`-Header der Anfrage. Aus einem Browser heraus ist das nicht
|
||||
zu missbrauchen — `Host` darf Skriptcode nicht setzen, und ein selbst
|
||||
gesetztes `X-Forwarded-Host` macht die Anfrage zu einer, die erst einen
|
||||
CORS-Vorabflug bestehen müsste, den die Anwendung nicht beantwortet. Das
|
||||
verbleibende Unbehagen ist das Vertrauensmodell insgesamt; dazu unten mehr.
|
||||
|
||||
---
|
||||
|
||||
## Was schon vorher stimmte
|
||||
@@ -218,6 +286,49 @@ selbst ausgestellt, falscher Name — statt bloß daran zu scheitern.
|
||||
|
||||
Bewusst nicht umgesetzt, mit Begründung.
|
||||
|
||||
**Die Kontosperre kann den Besitzer aussperren.** Acht falsche Passwörter
|
||||
sperren ein Konto für 15 Minuten, ohne dass der Anfragende belegen muss, wer
|
||||
er ist. Wer die Adresse des Administrators kennt, kann die Sperre alle 15
|
||||
Minuten erneuern und die Verwaltung praktisch dauerhaft blockieren.
|
||||
|
||||
Der naheliegende Ausweg — auch während der Sperre das Passwort prüfen und ein
|
||||
richtiges durchlassen — wurde geprüft und **verworfen**: Eine Prüfung kostet
|
||||
hier rund **128 MiB Arbeitsspeicher** (scrypt mit N = 2¹⁷). Wer sie beliebig
|
||||
oft auslösen darf, legt damit den Server lahm. Die Sperre gegen ein Konto
|
||||
einzutauschen wäre also eine Sperre gegen alle.
|
||||
|
||||
Was es stattdessen braucht, ist ein eigener Entwurf: eine ansteigende
|
||||
Verzögerung, die keinen Speicher hält, oder eine billige Vorprüfung vor dem
|
||||
teuren Hash — zusammen mit dem nächsten Punkt zu entscheiden.
|
||||
|
||||
**Die Bremsen lassen sich über `X-Forwarded-For` umgehen.** Der Absender wird
|
||||
aus diesem Header gelesen. Steht kein Proxy davor — und die mitgelieferte
|
||||
Compose-Konfiguration veröffentlicht den Port direkt —, wählt ihn jeder
|
||||
Aufrufer selbst und umgeht damit Login-Bremse und Feedback-Bremse. Die Zähler
|
||||
liegen zudem im Arbeitsspeicher eines Prozesses und sind nach einem Neustart
|
||||
weg.
|
||||
|
||||
Die Anwendung läuft in Next.js-Routen und kommt an die Adresse der
|
||||
Gegenstelle gar nicht heran — „im Zweifel die Socket-Adresse nehmen“ ist hier
|
||||
also keine Möglichkeit. Nötig ist eine ausdrückliche Einstellung, welchem
|
||||
Proxy zu trauen ist, und ein gemeinsamer Zähler, wenn mehr als eine Instanz
|
||||
läuft. Beides gehört mit dem Punkt davor in einen Entwurf und nicht in eine
|
||||
schnelle Änderung.
|
||||
|
||||
**Prisma hängt an einer Advisory.** `npm audit --omit=dev` meldet
|
||||
[GHSA-ggr8-5vv4-36mx](https://github.com/advisories/GHSA-ggr8-5vv4-36mx) in
|
||||
`deepmerge-ts@7.1.5`, mitgeschleppt über `@prisma/config`. Es geht um
|
||||
Stapelüberlauf beim Zusammenführen rekursiver Objekte.
|
||||
|
||||
Aus HTTP-Eingaben entsteht hier keine Prisma-Konfiguration, die praktische
|
||||
Ausnutzbarkeit ist also gering. Die Prisma-CLI liegt aber im Laufzeit-Abbild
|
||||
und läuft bei jedem Containerstart für die Migrationen — deshalb steht der
|
||||
Punkt hier und nicht unter „egal“. Eine Fassung ohne die Advisory gibt es
|
||||
noch nicht: 7.9.1 ist die neueste und hängt weiterhin an `deepmerge-ts@7.1.5`.
|
||||
`npm audit fix --force` würde auf Prisma 6 zurückstufen und ist deshalb keine
|
||||
Lösung. Zu tun: beobachten und aktualisieren, sobald es eine passende Fassung
|
||||
gibt.
|
||||
|
||||
**E-Mail-Verifizierung.** Es gibt keine — und sie würde derzeit auch nichts
|
||||
bringen. Die Anwendung kennt **keine Selbstregistrierung**: Es gibt nur
|
||||
`login`, `setup`, `logout` und `change-password`. Konten legt ein
|
||||
|
||||
Reference in New Issue
Block a user