Ergebnis der Durchsicht beider Bereiche (47 Dateien). Patientendaten ohne Anmeldung abrufbar -------------------------------------- admin/mailtemplate.php hatte keine Zugriffspruefung. Der Endpunkt liefert gerenderte Mailvorlagen und damit Vorname, Nachname, Geburtstag, Adresse, Medikamente, Anfragetext und den Anfragen-Link mit dem hash. Ein anonymer POST wurde bis in die Datenbank verarbeitet (geprueft mit ungueltiger templetid, es sind keine Daten geflossen). Jetzt check_admin_user() mit HTTP 401. Ausserdem ging die Exception-Meldung an den Aufrufer zurueck - sie geht jetzt ins Log. intern/meineanfragen.php filterte beim Detailaufruf ausschliesslich auf die anfrageid aus $_POST, ohne Bezug zum angemeldeten Benutzer - und setzte sie unmaskiert ins SQL. Jeder registrierte Patient konnte damit fremde Anfragen samt Geburtstag, Adresse und Telefonnummer lesen. Jetzt Prepared Statement und zusaetzlich an die E-Mail des angemeldeten Benutzers gebunden, wie in der Listenansicht derselben Datei. SQL-Injection ------------- admin/togoadmin.php: 14 Abfragen bauten $_GET/$_POST direkt in das SQL. Ganzzahlige Spalten bekommen einen (int)-Cast, damit die umgebenden mysqli-Schleifen unveraendert bleiben; alle INSERT- und UPDATE-Anweisungen mit Textwerten sind auf Prepared Statements umgestellt. create_time dort jetzt per NOW() statt PHP-date(). Ein abschliessender Scan ueber intern/, admin/ und zeiterfassung/ findet keine verkettete Nutzereingabe in SQL mehr. Zugriffspruefung ohne Wirkung ----------------------------- admin/anrufbeantworter.php und admin/kalender.php riefen check_admin_user() auf, werteten den Rueckgabewert aber nie aus. Die Funktion liefert bei fehlender Anmeldung nur null, sie bricht nicht ab - beide Seiten rendered fuer anonyme Besucher weiter. Daten flossen nicht ab, aber die Oberflaeche war sichtbar. Jetzt gleiches Muster wie admin/index.php. Tokens ohne Ablauf ------------------ Die Tabelle securitytokens hatte keine Ablaufspalte: ein erbeutetes Admin-Cookie galt unbegrenzt. Der Patientenbereich setzt 30 Tage. securitytokensHatAblaufspalte() prueft die Spalte zur Laufzeit, damit der Code vor und nach der Migration laeuft; admin/login.php setzt den Ablauf beim Anlegen, check_admin_user() beruecksichtigt ihn beim Lesen. Migration in admin/sql/. Die Cookie-Laufzeit war auf 365 Tage gesetzt und ist jetzt deckungsgleich mit dem Token. Entfernt -------- admin/phpinfo.php lieferte ohne Anmeldung 102 KB Serverkonfiguration. intern/admin.php, admin/admin.php sowie mailtemplatebody.php und mailtemplatebetreff.php in beiden Verzeichnissen waren tot (falscher relativer require-Pfad, HTTP 500) - die mailtemplate-Vorgaenger enthielten zudem rohe SQL-Injection mit Patientendaten. Keine der sechs Dateien wird irgendwo aufgerufen. admin/sql war als leere Datei statt als Verzeichnis angelegt. Kleinere Korrekturen -------------------- intern/authentifizierung.php und admin/passwortzuruecksetzen.php verglichen das Ergebnis von fetch() mit null statt false und pruefen den Ablauf des Codes jetzt in SQL statt mit strtotime()/time() - dieselbe Zeitzonenfalle wie bei den 2FA-Codes, hier in die harmlose Richtung. Ein toter password_hash()-Aufruf auf einer nie gesetzten Variablen ist entfallen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.8 KiB
PHP
108 lines
3.8 KiB
PHP
<?php
|
|
// admin/bootstrap.php
|
|
ob_start(); // fängt zufälligen Output ab, verhindert "headers already sent" Folgeschäden
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
session_start();
|
|
}
|
|
|
|
require_once(__DIR__ . "/../inc/config.inc.php");
|
|
require_once(__DIR__ . "/../inc/functions.inc.php");
|
|
|
|
$error_msg = "";
|
|
|
|
if (!empty($_POST['email']) && !empty($_POST['passwort'])) {
|
|
$email = $_POST['email'];
|
|
$passwort = $_POST['passwort'];
|
|
|
|
$statement = $pdo->prepare("SELECT * FROM users WHERE email = :email LIMIT 1");
|
|
$statement->execute(['email' => $email]);
|
|
$user = $statement->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($user && password_verify($passwort, $user['passwort'])) {
|
|
|
|
// Session IMMER setzen
|
|
session_regenerate_id(true);
|
|
$_SESSION['auth'] = [
|
|
'type' => 'admin',
|
|
'id' => (int)$user['id'],
|
|
];
|
|
|
|
// "Angemeldet bleiben" optional
|
|
if (!empty($_POST['angemeldet_bleiben'])) {
|
|
$identifier = bin2hex(random_bytes(16));
|
|
$securitytoken = bin2hex(random_bytes(32)); // stärker als random_string()
|
|
|
|
$hash = hash('sha256', $securitytoken);
|
|
|
|
// Ablaufdatum nur setzen, wenn die Spalte existiert - der Code laeuft
|
|
// damit vor und nach der Migration. Ablauf per NOW() in SQL, weil
|
|
// Webserver und DB-Server in verschiedenen Zeitzonen laufen.
|
|
if (securitytokensHatAblaufspalte($pdo)) {
|
|
$insert = $pdo->prepare("
|
|
INSERT INTO securitytokens (user_id, identifier, securitytoken, user_type, expires_at)
|
|
VALUES (:user_id, :identifier, :securitytoken, 'admin', DATE_ADD(NOW(), INTERVAL 30 DAY))
|
|
");
|
|
} else {
|
|
$insert = $pdo->prepare("
|
|
INSERT INTO securitytokens (user_id, identifier, securitytoken, user_type)
|
|
VALUES (:user_id, :identifier, :securitytoken, 'admin')
|
|
");
|
|
}
|
|
$insert->execute([
|
|
'user_id' => (int)$user['id'],
|
|
'identifier' => $identifier,
|
|
'securitytoken' => $hash
|
|
]);
|
|
|
|
$cookieOpts = [
|
|
'expires' => time() + 30*24*3600, // deckungsgleich mit dem Token-Ablauf
|
|
'path' => '/',
|
|
'secure' => true, // nur wenn HTTPS
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
];
|
|
setcookie('identifier', $identifier, $cookieOpts);
|
|
setcookie('securitytoken', $securitytoken, $cookieOpts);
|
|
}
|
|
|
|
header("Location: index.php");
|
|
exit;
|
|
} else {
|
|
$error_msg = "E-Mail oder Passwort war ungültig<br><br>";
|
|
}
|
|
}
|
|
|
|
$email_value = isset($_POST['email']) ? htmlentities($_POST['email']) : "";
|
|
include("templates/header.inc.php");
|
|
?>
|
|
|
|
|
|
<div class="container small-container-330 form-signin">
|
|
<form action="login.php" method="post">
|
|
<h2 class="form-signin-heading">Login</h2>
|
|
|
|
<?php
|
|
if(isset($error_msg) && !empty($error_msg)) {
|
|
echo $error_msg;
|
|
}
|
|
?>
|
|
<label for="inputEmail" class="sr-only">E-Mail</label>
|
|
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="E-Mail" value="<?php echo $email_value; ?>" required autofocus>
|
|
<label for="inputPassword" class="sr-only">Passwort</label>
|
|
<input type="password" name="passwort" id="inputPassword" class="form-control" placeholder="Passwort" required>
|
|
<div class="checkbox">
|
|
<label>
|
|
<input type="checkbox" value="remember-me" name="angemeldet_bleiben" value="1" checked> Angemeldet bleiben
|
|
</label>
|
|
</div>
|
|
<button class="btn btn-lg btn-primary btn-block" type="submit">Login</button>
|
|
<br>
|
|
<a href="passwortvergessen.php">Passwort vergessen</a>
|
|
</form>
|
|
|
|
</div> <!-- /container -->
|
|
|
|
|
|
<?php
|
|
include("templates/footer.inc.php")
|
|
?>
|