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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0c20698fd5
commit
91d8bf353e
@@ -38,6 +38,7 @@ use Deploymentcenter\Core\Webauthn\Attestation;
|
||||
use Deploymentcenter\Core\Webauthn\CredentialRepo;
|
||||
use Deploymentcenter\Core\Webauthn\HardwarePolicy;
|
||||
use Deploymentcenter\Core\Webauthn\WebauthnServer;
|
||||
use Deploymentcenter\Modules\License\RateLimiter;
|
||||
|
||||
// Kein publicCors: Diese Endpunkte arbeiten mit dem Sitzungscookie. Ein
|
||||
// "Access-Control-Allow-Origin: *" waere hier das Gegenteil dessen, was
|
||||
@@ -56,6 +57,16 @@ $db = Db::init();
|
||||
$repo = new CredentialRepo($db);
|
||||
$policy = new HardwarePolicy($db);
|
||||
|
||||
// Drosselung. login_options liess sich bisher unbegrenzt aufrufen; der
|
||||
// einzige Schutz war die Kontosperre, und die greift erst ab dem fuenften
|
||||
// Fehlversuch. Das Fenster ist weiter gefasst als am Anmeldeformular (30
|
||||
// statt 10), weil eine einzelne Anmeldung hier mehrere Aufrufe braucht -
|
||||
// Vorgaben holen, Antwort pruefen, bei einem Abbruch von vorn.
|
||||
$drossel = new RateLimiter($db, 30, 60, 'webauthn');
|
||||
if (!$drossel->check(Http::clientIp())) {
|
||||
Http::fail(429, 'rate_limited', 'Zu viele Anfragen. Bitte kurz warten.');
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@@ -177,6 +188,25 @@ switch ($action) {
|
||||
}
|
||||
Csrf::requireValid();
|
||||
|
||||
// Einen zweiten Faktor aufzunehmen ist die wirksamste Art, sich in
|
||||
// einem Konto einzunisten: Der eigene Schluessel gilt danach
|
||||
// dauerhaft und faellt niemandem auf. Deshalb genuegt hier eine
|
||||
// lebende Sitzung nicht - es braucht eine Authentifizierung aus den
|
||||
// letzten Minuten. Der Browser bekommt einen eigenen Code dafuer,
|
||||
// damit die Oberflaeche zum Sperrbildschirm schicken kann, statt
|
||||
// "verboten" zu melden.
|
||||
if (!Auth::reauthFresh()) {
|
||||
Http::fail(
|
||||
403,
|
||||
'reauth_required',
|
||||
'Diese Aenderung betrifft die Anmeldung selbst. Bitte bestaetigen Sie sie '
|
||||
. 'zuerst mit Ihrem Passwort.',
|
||||
null,
|
||||
['reauth_url' => '/login.php?step=unlock&weiter=' . rawurlencode('/index.php#sub-system-users')]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$userId = Auth::userId();
|
||||
|
||||
$challenge = WebauthnServer::challenge();
|
||||
@@ -205,6 +235,25 @@ switch ($action) {
|
||||
}
|
||||
Csrf::requireValid();
|
||||
|
||||
// Einen zweiten Faktor aufzunehmen ist die wirksamste Art, sich in
|
||||
// einem Konto einzunisten: Der eigene Schluessel gilt danach
|
||||
// dauerhaft und faellt niemandem auf. Deshalb genuegt hier eine
|
||||
// lebende Sitzung nicht - es braucht eine Authentifizierung aus den
|
||||
// letzten Minuten. Der Browser bekommt einen eigenen Code dafuer,
|
||||
// damit die Oberflaeche zum Sperrbildschirm schicken kann, statt
|
||||
// "verboten" zu melden.
|
||||
if (!Auth::reauthFresh()) {
|
||||
Http::fail(
|
||||
403,
|
||||
'reauth_required',
|
||||
'Diese Aenderung betrifft die Anmeldung selbst. Bitte bestaetigen Sie sie '
|
||||
. 'zuerst mit Ihrem Passwort.',
|
||||
null,
|
||||
['reauth_url' => '/login.php?step=unlock&weiter=' . rawurlencode('/index.php#sub-system-users')]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$userId = Auth::userId();
|
||||
|
||||
$challenge = Auth::takeChallenge();
|
||||
|
||||
Reference in New Issue
Block a user