Files
Deploymentcenter BotandClaude Opus 5 91d8bf353e fix(2.9.3): Der zweite Faktor war neben der Zeremonie umgehbar
Ergebnis einer beauftragten Sicherheitsbegutachtung. Die WebAuthn-
Zeremonie selbst ist nicht angefasst worden - sie war in Ordnung. Alle
Befunde lagen DANEBEN:

  Ein zweiter Faktor ist nur so stark wie der schwaechste Weg zum Konto,
  und Sitzungen, die aus ihm entstehen, erben seinen Schutz nicht.

KRITISCH
/api/setup/v1/login prueste auf totp_secret - eine Spalte, die kein
Anmeldeweg auswertet - und nicht auf dc_user_credentials. Ein
Administratorpasswort genuegte dort fuer ein setup:install-Token ohne
Hardwarekey. Neu: Konten mit zweitem Faktor abgewiesen, und die
Rollentrennung gilt symmetrisch. Der Docblock behauptete sie seit jeher;
erzwungen hat sie nichts.

HOCH
secondFactorRequired(), accountLockoutSeconds() und lockoutSeconds()
fingen jedes Throwable und fielen auf "kein Faktor" bzw. "nicht
gesperrt" zurueck. Gefangen wurden damit auch Verbindungsabbrueche und
Lock-Timeouts - jeder davon schaltete still fuer alle Konten auf
Passwort-allein. Toleriert werden jetzt nur noch 42S02 und 42S22.

HOCH
Fuenf Aktionen aenderten die Anmeldemittel ohne erneute Bestaetigung.
Eine uebernommene Sitzung konnte sich damit dauerhaft einnisten. Neu:
Authentifizierung aus den letzten zehn Minuten noetig, geprueft an
derselben Stelle, an der auch der Leerlauf sperrt.

WEITER
- Leerlauf 30 statt 120 Minuten, und GESPERRT statt beendet: AAL2 laesst
  zur Reaktivierung einen Faktor genuegen. Wer alle 30 Minuten den
  Hardwarekey vorzeigen muss, laesst ihn stecken.
- Migration 022: session_epoch. Ein Passwortwechsel beendet die uebrigen
  Sitzungen des Kontos, die eigene bleibt.
- Kontosperre wirkt auf der zweiten Stufe; Zaehler gegen unbegrenztes
  Raten im Zwischenzustand.
- Drosselung auf login.php (10/60s) und /api/auth/v1/ (30/60s).
- Anmeldefehler ununterscheidbar; die IP-Sperre darf benannt bleiben,
  sie sagt nichts ueber ein Konto.
- HSTS, TLS-Zwang VOR allen Routen, CSP. Die CSP traegt wegen 93
  Inline-Handlern nicht gegen XSS - das steht so im Kommentar.
- session.use_strict_mode, CSRF-Token bei der Anmeldung verworfen,
  Secure-Flag aus app.url statt aus $_SERVER['HTTPS'].
- rpId/expectedOrigin nicht mehr aus dem Host-Header. /api/health meldet
  unter checks.webauthn_anchor, ob der Anker steht.
- Notfalltoken laeuft nach 180 Tagen ab, geprueft nach dem Hashvergleich.
- config.example.php ohne echtes Passwort, verify_ssl auf true.
- Serverdaten.txt.bak mitgesperrt, dc_login_attempts raeumt auf.

BELEGT
188 von 188 Testfaellen gruen, darunter drei neue in der Gruppe
"haertung". Die Blockade einer veralteten Sitzung liess sich nicht in
der Suite pruefen - sie verlangt, das Zehn-Minuten-Fenster wirklich
abzuwarten. Dafuer gibt es ein Werkzeug ausserhalb; es belegt, dass der
Formularweg auf den Sperrbildschirm fuehrt, die Regel dabei unveraendert
bleibt, der JSON-Weg reauth_required meldet und das Passwort allein
wieder oeffnet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 14:58:31 +02:00

112 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""
Testlauf gegen die Testinstanz.
python tests/e2e/run.py # alles, mit Zuruecksetzen
python tests/e2e/run.py --gruppe sperre # nur eine Gruppe
python tests/e2e/run.py --nur roadmap # Namensfilter
python tests/e2e/run.py --kein-reset # bestehenden Datenbestand behalten
python tests/e2e/run.py --bericht bericht.json
python tests/e2e/run.py --log # bei Fehlern das Serverlog zeigen
Rueckgabewert: 0 wenn alles bestanden ist, 1 bei Fehlschlaegen, 2 wenn der
Lauf gar nicht erst starten konnte. scripts/deploy.py wertet genau das aus und
uebertraegt nichts auf die Produktion, solange hier etwas rot ist.
Testfaelle, die einen gemeldeten und noch offenen Befund festhalten, stehen in
bekannte_befunde.json. Sie laufen mit, erscheinen als BEK - und blockieren das
Deployment nicht. Besteht so ein Fall wieder, sagt der Lauf das ausdruecklich.
Gruppen:
sperre Zugangssperre und Webroot (.htaccess)
kern Zustand, OpenAPI, Migration, allgemeines Verhalten
token Token, Rechte, Provisionierung
bugtracker Melden, Arbeit holen, uebernehmen, abschliessen
roadmap Planung
fehlerstrom Ausnahmen aus Clients
lizenz Lizenzen und Entwicklungsschluessel
watchdog Lebenszeichen, Ereignisse, Auswertung
update Releases veroeffentlichen und finden
setup Erstinstallation
supervisor Befunde
hardware Welche Authenticatoren zugelassen sind
haertung Haertung der Anmeldewege
"""
import argparse
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import harness # noqa: E402
# Die Reihenfolge der Importe ist die Reihenfolge der Testfaelle.
import suite_sperre # noqa: E402,F401
import suite_kern # noqa: E402,F401
import suite_bugtracker # noqa: E402,F401
import suite_dienste # noqa: E402,F401
import suite_tafel # noqa: E402,F401
import suite_hardware # noqa: E402,F401
def main():
p = argparse.ArgumentParser(description="Testlauf gegen die Deploymentcenter-Testinstanz")
p.add_argument('--config', help="Pfad zu deploy_dev_config.json")
p.add_argument('--gruppe', action='append', help="nur diese Gruppe (mehrfach moeglich)")
p.add_argument('--nur', help="nur Testfaelle, deren Name diesen Text enthaelt")
p.add_argument('--kein-reset', dest='reset', action='store_false',
help="Datenbank nicht zuruecksetzen (dann master_token in der Konfiguration)")
p.add_argument('--bericht', help="Ergebnisse zusaetzlich als JSON ablegen")
p.add_argument('--log', action='store_true',
help="bei Fehlschlaegen das Serverlog der Testinstanz anzeigen")
p.add_argument('--stopp', action='store_true', help="beim ersten Fehlschlag abbrechen")
p.add_argument('--liste', action='store_true', help="nur die Testfaelle auflisten")
p.add_argument('--ipv4', action='store_true',
help="IPv4 erzwingen (sonst wird IPv6 einmal geprueft und bei "
"fehlender Route automatisch umgeschaltet)")
args = p.parse_args()
cfg = harness.lade_config(args.config)
if args.liste:
gruppe = None
for fall in harness.FAELLE:
if fall.gruppe != gruppe:
gruppe = fall.gruppe
print("")
print("%s:" % gruppe)
print(" %s" % fall.name)
print("")
print("%d Testfaelle" % len(harness.FAELLE))
return 0
code = harness.run(cfg,
gruppen=args.gruppe,
muster=args.nur,
reset=args.reset,
bericht_datei=args.bericht,
stoppen=args.stopp,
adressfamilie='ipv4' if args.ipv4 else None)
if code == 1 and args.log:
# Das Log liegt hinter der Sperre; ohne diesen Weg endet ein
# fehlgeschlagener Lauf bei "HTTP 500" und dem Raten, was dahinter
# stand.
client = harness.Client(cfg['base_url'],
cfg['basic_auth']['user'], cfg['basic_auth']['pass'])
r = client.post('/testctl.php', body={'action': 'log', 'lines': 40},
token=cfg['keys']['shared_key'])
print("")
print("--- Serverlog der Testinstanz " + "-" * 40)
for datei, zeilen in (r.feld('log', default={}) or {}).items():
print("[%s]" % datei)
for zeile in zeilen:
print(" %s" % zeile)
return code
if __name__ == '__main__':
sys.exit(main())