Konsolidierung: Security-Härtung + .gitignore

- TOTP-Secret wird verschlüsselt-at-rest gespeichert (Crypto, Klartext-Fallback)
- forgot_password.php: Throttle (3/15min je Session) gegen Reset-Spam
- .gitignore für vendor/, composer.lock, phpunit-Cache, Updater-Laufzeitdaten
- Verifiziert: PHPUnit 15/15 grün, PHPStan ohne Fehler
This commit is contained in:
Claude 2026-06-06 05:38:40 +00:00
parent c63c5e78f0
commit 546accc185
No known key found for this signature in database
3 changed files with 26 additions and 3 deletions

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Dev-/Test-Abhängigkeiten (Laufzeit braucht KEIN composer)
/vendor/
composer.lock
.phpunit.result.cache
.phpunit.cache/
# Updater-Laufzeitdaten
/updater/storage/.version
/updater/storage/.maintenance
/updater/storage/.update-progress
/updater/storage/.update.zip
/updater/storage/.update-staging/
/updater/storage/.migrations-lock
/updater/storage/updater-settings.json

View file

@ -24,9 +24,16 @@ $success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Einfacher Throttle: max. 3 Anfragen pro 15 Minuten je Session (gegen Spam)
$now = time();
$rl = array_values(array_filter($_SESSION['pwreset_times'] ?? [], fn($t) => ($now - $t) < 900));
if (count($rl) >= 3) {
$error = 'Zu viele Anfragen. Bitte warten Sie einige Minuten.';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = __('error_email_invalid');
} else {
$rl[] = $now;
$_SESSION['pwreset_times'] = $rl;
$user = $db->fetchOne("SELECT * FROM users WHERE email = ? AND is_active = 1 AND password_hash IS NOT NULL", [$email]);
// Always show success (don't reveal whether email exists)

View file

@ -1,6 +1,7 @@
<?php
require_once __DIR__ . '/Totp.php';
require_once __DIR__ . '/Notifier.php';
require_once __DIR__ . '/Crypto.php';
class Auth {
private $db;
@ -117,7 +118,8 @@ class Auth {
return false;
}
// Entweder gueltiger TOTP-Code ODER ein Recovery-/Backup-Code
$ok = Totp::verify($user['totp_secret'], $code);
// (Secret wird verschluesselt gespeichert; Klartext-Fallback via decrypt)
$ok = Totp::verify(Crypto::decrypt($user['totp_secret']), $code);
if (!$ok && $this->consumeBackupCode($user, $code)) {
$ok = true;
$this->writeAuditLog($user['id'], 'user_login_backup_code', 'user', $user['id'], 'Login per Recovery-Code');
@ -143,7 +145,7 @@ class Auth {
$hashes = array_map(function ($c) { return hash('sha256', $c); }, $codes);
$this->db->query(
"UPDATE users SET totp_secret = ?, totp_enabled = 1, totp_backup_codes = ? WHERE id = ?",
[$secret, json_encode($hashes), $userId]
[Crypto::encrypt($secret), json_encode($hashes), $userId]
);
$this->writeAuditLog($userId, 'totp_enabled', 'user', $userId, '2FA aktiviert');
return $codes;