R193-02: Truthiness-Bypass bei den drei Haken (HIGH)

Befund der Pentesterin, Prod-Blocker. Der Eskalations-Guard fragte
`=== true`, die Zuweisung fragte auf Truthiness. Zwischen diesen beiden
Lesarten passte der Angriff: {"hasDeveloperAccess":"ja"} sah fuer den
Guard nach keinem Rechtezuwachs aus (kein 403) und fuer die Zuweisung nach
"gesetzt" (Rolle drauf). Ein Konto mit users:update, das developer:access
nicht haelt, konnte damit einem anderen Konto Vollzugriff geben.

Damit war die Kernentscheidung aus Etappe 1 ausgehebelt: developer:access
und audit:admin sollten ausdruecklich nur ueber die Kommandozeile
erstmalig vergebbar sein. Der Bypass machte sie wieder ueber ein
Browser-Feld erreichbar.

Es ist genau das Muster, das ich der Pentesterin eine Runde vorher selbst
beschrieben habe - eine Absicherung, die nur in einer von zwei Kopien
ankommt, ist keine. Nur dass es diesmal nicht zwei Dateien waren, sondern
zwei Auffassungen desselben Wertes in derselben Datei.

normalisiereHaken() prueft die drei Felder einmal streng auf Boolean,
alles andere 400. Guard und Zuweisung arbeiten danach auf demselben
geprueften Objekt - es gibt nur noch eine Lesart. Zusaetzlich liest
setzeVersteckteRolle strikt === true / === false, damit sich die Lesart
auch dann nicht spaltet, wenn die Funktion kuenftig von woanders gerufen
wird.

Gleiche Bauart nebenan: isActive und isServiceAccount steuern ebenfalls
ein Gate, das strikt auf boolean prueft - ein "ja" rutschte daran vorbei,
ohne das Gate auszuloesen (u.a. die audit:admin-Huerde fuers
Dienstkonto-Kennzeichen), und lief dann in einen Prisma-Fehler. Kein
Bypass, aber ein 500 fuer eine falsche Eingabe. Jetzt alle fuenf
Boolean-Felder einheitlich geprueft.

Nachgeprueft: ihre vier Vektoren plus 30 weitere Kombinationen ueber alle
drei Haken - alle 400, Rollen unveraendert. Derselbe Angriff ueber
POST /users - 400, kein Konto angelegt. Gegenprobe: Wer gdpr:* selbst
haelt, setzt den Haken mit echtem true weiterhin erfolgreich.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 02:45:11 +02:00
co-authored by Claude Opus 5
parent 75d009138d
commit f964610af7
4 changed files with 137 additions and 16 deletions
+39
View File
@@ -101,6 +101,45 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
- [x] **🚨 R193-02: Truthiness-Bypass bei den drei Haken (HIGH)** (2026-09-09)
- Befund der Pentesterin, **Prod-Blocker**: Der Eskalations-Guard fragte
`=== true`, die Zuweisung fragte auf Truthiness. Zwischen diesen beiden
Lesarten passte der Angriff: `{"hasDeveloperAccess":"ja"}` sah für den
Guard nach keinem Rechtezuwachs aus (also kein 403) und für die Zuweisung
nach „gesetzt" (also Rolle drauf). Ein Konto mit `users:update`, das
`developer:access` **nicht** hält, konnte damit einem anderen Konto
Vollzugriff geben — und sich anmelden.
- Damit war die Kernentscheidung aus Etappe 1 ausgehebelt: `developer:access`
und `audit:admin` sollten ausdrücklich **nur über die Kommandozeile**
erstmalig vergebbar sein. Der Bypass machte sie wieder über ein
Browser-Feld erreichbar.
- Es ist exakt das Muster, das ich der Pentesterin eine Runde vorher selbst
beschrieben hatte: *„Eine Absicherung, die nur in einer von zwei Kopien
ankommt, ist keine."* Nur dass es diesmal nicht zwei Dateien waren,
sondern zwei **Auffassungen desselben Wertes** in derselben Datei.
- Fix: `normalisiereHaken()` prüft die drei Felder einmal streng auf Boolean
(alles andere 400), und **Guard und Zuweisung arbeiten danach auf
demselben geprüften Objekt** — es gibt nur noch eine Lesart. Zusätzlich
liest `setzeVersteckteRolle` strikt `=== true` / `=== false`, damit sich
die Lesart auch dann nicht spaltet, wenn die Funktion künftig von
woanders gerufen wird.
- **Gleiche Bauart nebenan gefunden:** `isActive` und `isServiceAccount`
steuern ebenfalls ein Gate, das strikt auf `boolean` prüft — ein `"ja"`
rutschte daran vorbei, ohne das Gate (u. a. die `audit:admin`-Hürde für
das Dienstkonto-Kennzeichen) auszulösen, und lief dann in einen
Prisma-Fehler. Kein Bypass, weil der Schreibvorgang scheiterte, aber ein
500 für eine schlicht falsche Eingabe. Jetzt alle fünf Boolean-Felder
einheitlich geprüft → 400.
- Nachgeprüft: Ihre vier Vektoren plus 30 weitere Kombinationen
(`1`, `"true"`, `"false"`, `{}`, `[1]`, `null`, `"0"`, `-1`, `2.5` × drei
Haken) → **alle 400, Rollen unverändert**. Derselbe Angriff über
`POST /users` → 400, **kein Konto angelegt**. Gegenprobe: Ein Täter, der
`gdpr:*` selbst hält, setzt den Haken mit echtem `true` weiterhin
erfolgreich (200), und `hasDeveloperAccess:true` bleibt 403.
- Dateien: `src/services/rechte.service.ts`, `src/services/user.service.ts`,
`src/controllers/user.controller.ts`
- [x] **🤐 R192-01: Fehlermeldungen, die für Entwickler geschrieben sind** (2026-09-09)
- Befund der Pentesterin: `PUT /users/:id {"roleIds":{}}` gab 400 mit dem
rohen JS-Fehler `object is not iterable (cannot read property