Sicherungskopien
----------------
intern/neueanfrage-old1.php und -old2.php waren toter Code, aber als
.php im Web-Root oeffentlich aufrufbar - inklusive eigener DB- und
Mailzugriffe. Auf sie verweist nichts, sie sind jetzt entfernt.
Der Stand bleibt ueber die Git-Historie erreichbar.
Datenbank-Zugangsdaten
----------------------
Host, Benutzer, Passwort und Datenbankname standen im Klartext in
inc/config.inc.php UND in zeiterfassung/inc/config.inc.php, beide
versioniert. Sie liegen jetzt in inc/credentials.php, das per
.gitignore ausgenommen ist; beide Configs laden es und brechen mit
HTTP 500 und einem Log-Eintrag ab, wenn es fehlt, statt sich still
ohne Zugangsdaten zu verbinden. inc/credentials.example.php ist als
versionierte Vorlage dabei.
FTP-Zugangsdaten
----------------
.vscode/ftp-sync.json und .vscode/sftp.json enthalten das
FTP-Passwort und waren versioniert - die .gitignore-Eintraege dafuer
gab es zwar, sie greifen bei bereits getrackten Dateien aber nicht.
Beide sind jetzt per "git rm --cached" aus der Versionierung genommen
und bleiben lokal liegen. Die passenden .example-Dateien existieren
bereits.
Verzeichnisschutz
-----------------
inc/ und zeiterfassung/inc/ enthalten ausschliesslich Includes, waren
als Teil des Web-Roots aber direkt per URL abrufbar. Beide bekommen
eine .htaccess, die den HTTP-Zugriff verweigert. PHP-includes sind
davon nicht betroffen, und per HTTP greift nichts auf diese
Verzeichnisse zu.
Wichtig: das Passwort steht weiterhin in der Git-Historie und wurde
bereits gepusht. Diese Aenderung verhindert nur die Weiterverbreitung.
Wirksam entschaerft ist es erst, wenn DB- und FTP-Passwort gewechselt
werden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei zusammenhaengende Punkte aus der Analyse des Login-Problems.
1. PHP-Fehler wurden an Besucher ausgegeben
display_errors war in impfwarteliste.php (oeffentlich), intern/
impfwarteliste.php, intern/neueanfrage.php, den beiden neueanfrage-old
Sicherungen und zeiterfassung/api/vacations.php fest eingeschaltet.
Die oeffentliche Impfwarteliste zeigte Patienten bei einem Fatal Error
sogar Meldung, Dateipfad und Zeilennummer. Ueberall auf log_errors
umgestellt: E_ALL wird weiterhin vollstaendig erfasst, landet aber im
Server-Log statt auf der Seite. Der Shutdown-Handler der Impfwarteliste
protokolliert die Details und zeigt nur noch einen neutralen Hinweis.
Auch register.php gab die rohe Exception-Meldung aus.
2. SendMailMessageSilent() verschluckte jeden Fehler
Der catch-Block war leer. Ein SMTP-Ausfall war dadurch von aussen nicht
von einem falschen Code zu unterscheiden - der Benutzer landete auf
verify_2fa.php und wartete auf eine Mail, die nie kam. Die Funktion
protokolliert jetzt und liefert einen bool zurueck. login.php wertet
das aus, nimmt bei Fehlschlag den 2FA-Datensatz und die Session-Vormerkung
zurueck und sagt es auf der Login-Seite, statt weiterzuleiten.
Nebenbei entfernt: ein uebrig gebliebenes echo, das den Mailserver-Namen
mitten in die Seite schrieb, sowie ein zweites mysqli_fetch_assoc() auf
demselben Result, das nur NULL liefern konnte.
3. register.php verschickte keine Bestaetigungsmail
mailreg blieb 0, jeder neue Benutzer landete nach dem Login auf der
Aufforderung, die Authentifizierung selbst anzustossen. Die Mail geht
jetzt direkt nach der Registrierung raus. Erzeugung und Text liegen in
der neuen Funktion sendeAuthentifizierungsMail(), die authmeldung.php
ebenfalls benutzt - dort wurde der Rueckgabewert des Versands bisher
einer Variablen zugewiesen und nie ausgewertet, die Seite meldete
Erfolg auch bei fehlgeschlagenem Versand.
Der Versand laeuft nach dem commit(), deshalb prueft der catch-Block in
register.php jetzt inTransaction(), bevor er rollBack() aufruft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Pruefung war auskommentiert, Reset-Links galten damit unbegrenzt,
obwohl die E-Mail "innerhalb der naechsten 24 Stunden" verspricht.
Sie wird jetzt in SQL ausgewertet statt mit strtotime()/time():
passwortcode_time wird in passwortvergessen.php mit NOW() gesetzt, also
mit der Uhr des DB-Servers (Europe/Berlin), waehrend der Webserver in
UTC laeuft. Eine Pruefung in PHP waere um diese zwei Stunden falsch -
dieselbe Ursache, die schon die 2FA-Codes unbrauchbar gemacht hat.
So benutzen Setzen und Pruefen dieselbe Uhr.
Ausserdem: fetch() liefert false und nicht null, wenn kein Benutzer
gefunden wird. Die Bedingung $user === null hat deshalb nie gegriffen,
und der Code lief mit false weiter in den Array-Zugriff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
login.php erzeugte expires_at mit PHPs date(), verify_2fa.php verglich
gegen MySQLs NOW(). Webserver (secureserver.net) und Datenbankserver
(mysql2fda.netcup.net) liegen bei verschiedenen Anbietern und sind
unabhaengig voneinander konfiguriert. Laeuft PHP in UTC und MySQL in
Europe/Berlin, liegt expires_at (PHP-Zeit + 5 Minuten) zwei Stunden
vor NOW() - jeder Code gilt sofort als abgelaufen und wird als
"Falscher oder abgelaufener Code" abgewiesen.
Die Ablaufzeit wird jetzt beim INSERT per DATE_ADD(NOW(), INTERVAL 5
MINUTE) berechnet. Erzeugung und Pruefung benutzen damit dieselbe Uhr,
unabhaengig davon, wie die beiden Server eingestellt sind.
Ausserdem:
- Der eingegebene Code wird auf Ziffern reduziert. Aus HTML-Mails
kopierte Codes schleppen oft Leerzeichen oder geschuetzte
Leerzeichen mit, die den Hash-Vergleich scheitern liessen.
- Abgelaufener Code und falscher Code werden getrennt gemeldet, damit
ein solcher Fall kuenftig ohne Raten eingegrenzt werden kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
index.php und neueanfrage.php gaben das Header-Template aus, bevor
check_intern_user() aufgerufen wurde. Dadurch schlugen setcookie(),
session_regenerate_id() und header('Location: login.php') mit
"headers already sent" fehl: die Session wurde geloescht ohne neues
Cookie zu setzen, der rotierte Remember-Token landete nur in der DB,
und die Weiterleitung blieb wirkungslos. Ergebnis war eine halb
gerenderte Seite statt des Logins. Beide Seiten puffern jetzt mit
ob_start(), wie login.php und verify_2fa.php es bereits tun.
Weitere Fehler im selben Ablauf:
- passwortvergessen.php uebergab $con (mysqli) an SendMailMessage(),
das PDO erwartet -> fataler TypeError, "Passwort vergessen" brach
immer mit HTTP 500 ab und verschickte nie eine Mail.
- logout.php war aus dem Admin-Bereich kopiert und loeschte weder
intern_securitytokens noch die Cookies remember_device /
remember_device_token. Der Logout war damit wirkungslos, weil
check_intern_user() sofort wieder ueber das Cookie anmeldete.
- is_checked_in_index() pruefte das Admin-Cookie 'identifier' statt
'remember_device'.
- index.php gab das undefinierte $email_value im Login-Formular aus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>