From 864218c1b8e49a24064274072f29859070b38759 Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Wed, 2 Sep 2026 23:48:45 +0200 Subject: [PATCH] Zugriffsluecken und SQL-Injection in intern/ und admin/ beheben 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 --- admin/admin.php | 30 ----- admin/anrufbeantworter.php | 9 ++ admin/kalender.php | 9 ++ admin/login.php | 20 ++- admin/mailtemplate.php | 14 ++- admin/mailtemplatebetreff.php | 19 --- admin/mailtemplatebody.php | 60 --------- admin/passwortzuruecksetzen.php | 11 +- admin/phpinfo.php | 6 - admin/sql | 0 .../2026-09-02_securitytokens_expires_at.sql | 20 +++ admin/togoadmin.php | 119 +++++++++++++++--- inc/functions.inc.php | 36 +++++- intern/admin.php | 30 ----- intern/authentifizierung.php | 14 ++- intern/mailtemplatebetreff.php | 19 --- intern/mailtemplatebody.php | 60 --------- intern/meineanfragen.php | 21 +++- 18 files changed, 238 insertions(+), 259 deletions(-) delete mode 100644 admin/admin.php delete mode 100644 admin/mailtemplatebetreff.php delete mode 100644 admin/mailtemplatebody.php delete mode 100644 admin/phpinfo.php delete mode 100644 admin/sql create mode 100644 admin/sql/2026-09-02_securitytokens_expires_at.sql delete mode 100644 intern/admin.php delete mode 100644 intern/mailtemplatebetreff.php delete mode 100644 intern/mailtemplatebody.php diff --git a/admin/admin.php b/admin/admin.php deleted file mode 100644 index 2ca8b0e..0000000 --- a/admin/admin.php +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - diff --git a/admin/anrufbeantworter.php b/admin/anrufbeantworter.php index 071eb13..aeafc3b 100644 --- a/admin/anrufbeantworter.php +++ b/admin/anrufbeantworter.php @@ -9,6 +9,15 @@ $user = check_admin_user(); include("templates/header.inc.php"); +// check_admin_user() liefert bei fehlender Anmeldung nur null zurueck und +// bricht nicht ab. Ohne diese Pruefung lief die Seite fuer anonyme +// Besucher weiter (und $user['vorname'] warf eine Warnung). +if (!$user) { + echo "

Erst anmelden: Login


"; + include("templates/footer.inc.php"); + exit; +} + ?>
diff --git a/admin/kalender.php b/admin/kalender.php index a90cd41..d9c164a 100644 --- a/admin/kalender.php +++ b/admin/kalender.php @@ -9,6 +9,15 @@ $user = check_admin_user(); include("templates/header.inc.php"); +// check_admin_user() liefert bei fehlender Anmeldung nur null zurueck und +// bricht nicht ab. Ohne diese Pruefung lief die Seite fuer anonyme +// Besucher weiter (und $user['vorname'] warf eine Warnung). +if (!$user) { + echo "

Erst anmelden: Login


"; + include("templates/footer.inc.php"); + exit; +} + echo '
'; function build_calendar($month, $year) { diff --git a/admin/login.php b/admin/login.php index 1110237..efa9bd2 100644 --- a/admin/login.php +++ b/admin/login.php @@ -34,10 +34,20 @@ if (!empty($_POST['email']) && !empty($_POST['passwort'])) { $hash = hash('sha256', $securitytoken); - $insert = $pdo->prepare(" - INSERT INTO securitytokens (user_id, identifier, securitytoken, user_type) - VALUES (:user_id, :identifier, :securitytoken, 'admin') - "); + // 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, @@ -45,7 +55,7 @@ if (!empty($_POST['email']) && !empty($_POST['passwort'])) { ]); $cookieOpts = [ - 'expires' => time() + 365*24*3600, + 'expires' => time() + 30*24*3600, // deckungsgleich mit dem Token-Ablauf 'path' => '/', 'secure' => true, // nur wenn HTTPS 'httponly' => true, diff --git a/admin/mailtemplate.php b/admin/mailtemplate.php index f95bb8c..ab5012d 100644 --- a/admin/mailtemplate.php +++ b/admin/mailtemplate.php @@ -4,6 +4,15 @@ require_once __DIR__ . '/../inc/functions.inc.php'; header('Content-Type: application/json'); +// Dieser Endpunkt liefert gerenderte Mailvorlagen aus und damit Name, +// Geburtstag, Adresse, Medikamente und den Anfragetext des Patienten. +// Ohne diese Pruefung war er fuer jeden ohne Anmeldung abrufbar. +if (!check_admin_user()) { + http_response_code(401); + echo json_encode(['error' => 'Nicht angemeldet']); + exit; +} + try { $templetid = $_POST['templetid'] ?? ''; $anfrageid = (int)($_POST['anfrageid'] ?? 0); @@ -38,8 +47,11 @@ try { ]); } catch (Throwable $e) { + // Interne Meldungen nur ins Log - sie verraten sonst Tabellen-, + // Spalten- und Datensatznamen an den Aufrufer. + error_log('mailtemplate.php: ' . $e->getMessage()); http_response_code(500); echo json_encode([ - 'error' => $e->getMessage() + 'error' => 'Die Vorlage konnte nicht geladen werden.' ]); } \ No newline at end of file diff --git a/admin/mailtemplatebetreff.php b/admin/mailtemplatebetreff.php deleted file mode 100644 index 9383ae9..0000000 --- a/admin/mailtemplatebetreff.php +++ /dev/null @@ -1,19 +0,0 @@ - \ No newline at end of file diff --git a/admin/mailtemplatebody.php b/admin/mailtemplatebody.php deleted file mode 100644 index 634b767..0000000 --- a/admin/mailtemplatebody.php +++ /dev/null @@ -1,60 +0,0 @@ - \ No newline at end of file diff --git a/admin/passwortzuruecksetzen.php b/admin/passwortzuruecksetzen.php index 7d30420..7f76531 100644 --- a/admin/passwortzuruecksetzen.php +++ b/admin/passwortzuruecksetzen.php @@ -13,16 +13,21 @@ $userid = $_GET['userid']; $code = $_GET['code']; //Abfrage des Nutzers -$statement = $pdo->prepare("SELECT * FROM users WHERE id = :userid"); +$statement = $pdo->prepare("SELECT *, (passwortcode_time IS NOT NULL AND passwortcode_time > DATE_SUB(NOW(), INTERVAL 24 HOUR)) AS code_noch_gueltig FROM users WHERE id = :userid"); $result = $statement->execute(array('userid' => $userid)); $user = $statement->fetch(); //Überprüfe dass ein Nutzer gefunden wurde und dieser auch ein Passwortcode hat -if($user === null || $user['passwortcode'] === null) { +// fetch() liefert false und nicht null, wenn kein Benutzer gefunden wird - +// die alte Bedingung griff daher nie und lief mit false in den Array-Zugriff. +if(!$user || $user['passwortcode'] === null) { error("Der Benutzer wurde nicht gefunden oder hat kein neues Passwort angefordert."); } -if($user['passwortcode_time'] === null || strtotime($user['passwortcode_time']) < (time()-24*3600) ) { +// Ablauf in SQL pruefen: passwortcode_time wird mit NOW() gesetzt (Uhr des +// DB-Servers), waehrend PHP auf dem Webserver in einer anderen Zeitzone +// laeuft. strtotime()/time() lag hier um genau diese Differenz daneben. +if(!$user['code_noch_gueltig']) { error("Dein Code ist leider abgelaufen. Bitte benutze die Passwort vergessen Funktion erneut."); } diff --git a/admin/phpinfo.php b/admin/phpinfo.php deleted file mode 100644 index c9121e7..0000000 --- a/admin/phpinfo.php +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/admin/sql b/admin/sql deleted file mode 100644 index e69de29..0000000 diff --git a/admin/sql/2026-09-02_securitytokens_expires_at.sql b/admin/sql/2026-09-02_securitytokens_expires_at.sql new file mode 100644 index 0000000..80ca5b7 --- /dev/null +++ b/admin/sql/2026-09-02_securitytokens_expires_at.sql @@ -0,0 +1,20 @@ +-- Ablaufdatum fuer "Angemeldet bleiben"-Tokens des Admin- und +-- Zeiterfassungsbereichs. Bisher hatte die Tabelle keine Ablaufspalte: ein +-- einmal erbeutetes Cookie war unbegrenzt gueltig. Der interne +-- Patientenbereich setzt 30 Tage (Tabelle intern_securitytokens). +-- +-- Der Code laeuft vor und nach dieser Migration: securitytokensHatAblaufspalte() +-- in inc/functions.inc.php prueft die Spalte zur Laufzeit. Nach dem Einspielen +-- greift der Ablauf automatisch, ohne weiteres Deployment. +-- +-- Bestehende Tokens bekommen 30 Tage ab jetzt; NULL bliebe unbegrenzt gueltig. + +ALTER TABLE securitytokens + ADD COLUMN expires_at DATETIME NULL DEFAULT NULL AFTER user_type; + +UPDATE securitytokens + SET expires_at = DATE_ADD(NOW(), INTERVAL 30 DAY) + WHERE expires_at IS NULL; + +-- Gelegentlich aufraeumen: +-- DELETE FROM securitytokens WHERE expires_at IS NOT NULL AND expires_at < NOW(); diff --git a/admin/togoadmin.php b/admin/togoadmin.php index 5b11a97..c2832ac 100644 --- a/admin/togoadmin.php +++ b/admin/togoadmin.php @@ -125,7 +125,7 @@ if(!check_worker()){ ?> NummerPersonIP-AdresseAnlegungsdatumAktion"; @@ -171,7 +171,7 @@ if(!check_worker()){ ## EDIT EinzelTermin }elseif($_GET["a"] == "togoedittermin" ){ - $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin WHERE terminid = ". $_GET["id"] . ""); + $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin WHERE terminid = " . (int)$_GET["id"]); while ($row = $queryinsert->fetch_assoc()) { $date = $row["date"]; $start = $row["start"]; @@ -223,7 +223,7 @@ if(!check_worker()){ ### Allen im Termin eine E-Mail senden }else if($_GET["a"] == "togosendmail" ){ - $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin WHERE terminid = ". $_GET["id"] . ""); + $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin WHERE terminid = " . (int)$_GET["id"]); while ($row = $queryinsert->fetch_assoc()) { $date = $row["date"]; $start = $row["start"]; @@ -276,7 +276,7 @@ if(!check_worker()){ ### EDIT Serientermin }else if($_GET["a"] == "togoeditserientermin" ){ - $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin WHERE terminid = ". $_GET["id"] . ""); + $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin WHERE terminid = " . (int)$_GET["id"]); while ($row = $queryinsert->fetch_assoc()) { $date = $row["date"]; $start = $row["start"]; @@ -437,7 +437,7 @@ if(!check_worker()){ if($_GET["a"] == "removeserie" ){ - $sqlstring = "DELETE FROM togo_terminvorgaben WHERE vorgabenid = ". $_GET["id"] . ""; + $sqlstring = "DELETE FROM togo_terminvorgaben WHERE vorgabenid = " . (int)$_GET["id"]; $query = mysqli_query($con,$sqlstring); if($query){ @@ -477,7 +477,7 @@ if(!check_worker()){ } if($_GET["a"] == "deletetogotermin" ){ - $sqlstring = "DELETE FROM togo_termin WHERE terminid = ". $_GET["id"] . ""; + $sqlstring = "DELETE FROM togo_termin WHERE terminid = " . (int)$_GET["id"]; $query = mysqli_query($con,$sqlstring); if($query){ @@ -496,7 +496,18 @@ if(!check_worker()){ $standort = $_POST["standort"]; $error = $_POST["error"]; $warning = $_POST["warning"]; - $queryinsert = mysqli_query($con, "INSERT INTO togo_terminvorgaben (date, start, ende,standort,error,warning) VALUES ('".$date."', '".$start."', '".$end."', '".$standort."', '".$error."', '".$warning."')"); + $stmt = $pdo->prepare(" + INSERT INTO togo_terminvorgaben (date, start, ende, standort, error, warning) + VALUES (:date, :start, :ende, :standort, :error, :warning) +"); +$queryinsert = $stmt->execute([ + 'date' => $date, + 'start' => $start, + 'ende' => $end, + 'standort' => $standort, + 'error' => $error, + 'warning' => $warning +]); // Termin wird gespeichert if($queryinsert){ @@ -518,7 +529,7 @@ if(!check_worker()){ WHERE standort IS NULL OR COUNT = '-1'"); }else{ - $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin_teilnehmer WHERE terminid = ". $_POST["id"] . ""); + $queryinsert = mysqli_query($con, "SELECT * FROM togo_termin_teilnehmer WHERE terminid = " . (int)$_POST["id"]); } while ($row = $queryinsert->fetch_assoc()) { @@ -534,7 +545,7 @@ if(!check_worker()){ } if($_POST["abgesagten"] == "1"){ $id = $row["terminteilnehmerid"]; - $sqldeletestring = "DELETE FROM togo_termin_teilnehmer WHERE terminteilnehmerid = ". $id . ""; + $sqldeletestring = "DELETE FROM togo_termin_teilnehmer WHERE terminteilnehmerid = " . (int)$id; $querydelete = mysqli_query($con,$sqldeletestring); echo "Benutzer $id gelöscht
"; @@ -560,7 +571,20 @@ if(!check_worker()){ $count = $_POST["count"]; // Termin Änderung speichern - $queryinsert = mysqli_query($con, "UPDATE togo_termin SET date='".$date."', start='".$start."', count='".$count."', end='".$end."', standort='".$standort."', error= '".$error."', warning='".$warning."' WHERE terminid='".$terminid."'"); + $stmt = $pdo->prepare(" + UPDATE togo_termin SET date = :date, start = :start, count = :count, end = :end, + standort = :standort, error = :error, warning = :warning WHERE terminid = :terminid +"); +$queryinsert = $stmt->execute([ + 'date' => $date, + 'start' => $start, + 'count' => $count, + 'end' => $end, + 'standort' => $standort, + 'error' => $error, + 'warning' => $warning, + 'terminid' => $terminid +]); if($queryinsert){ @@ -585,7 +609,18 @@ if(!check_worker()){ // Termin Änderung speichern $endTime = strtotime($start) + ($folgelaenge*60); $ende = date('H:i:s', $endTime); - $queryinsert = mysqli_query($con, "INSERT INTO togo_termin (date, start, end,standort,error,warning) VALUES ('".$date."', '".$start."', '".$ende."', '".$standort."', '".$error."', '".$warning."')"); + $stmt = $pdo->prepare(" + INSERT INTO togo_termin (date, start, end, standort, error, warning) + VALUES (:date, :start, :ende, :standort, :error, :warning) +"); +$queryinsert = $stmt->execute([ + 'date' => $date, + 'start' => $start, + 'ende' => $ende, + 'standort' => $standort, + 'error' => $error, + 'warning' => $warning +]); $start = $ende; @@ -610,7 +645,20 @@ if(!check_worker()){ $warning = $_POST["warning"]; $terminid = $_POST["terminid"]; $count = $_POST["count"]; - $queryinsert = mysqli_query($con, "UPDATE togo_termin SET date='".$date."', start='".$start."', ende='".$end."',count='".$count."', standort='".$standort."', error= '".$error."', warning='".$warning."' WHERE terminid='".$terminid."'"); + $stmt = $pdo->prepare(" + UPDATE togo_termin SET date = :date, start = :start, ende = :ende, count = :count, + standort = :standort, error = :error, warning = :warning WHERE terminid = :terminid +"); +$queryinsert = $stmt->execute([ + 'date' => $date, + 'start' => $start, + 'ende' => $end, + 'count' => $count, + 'standort' => $standort, + 'error' => $error, + 'warning' => $warning, + 'terminid' => $terminid +]); // Termin wird angelegt @@ -631,7 +679,18 @@ if(!check_worker()){ $standort = $_POST["standort"]; $error = $_POST["error"]; $warning = $_POST["warning"]; - $queryinsert = mysqli_query($con, "INSERT INTO togo_termin (date, start, end,standort,error,warning) VALUES ('".$date."', '".$start."', '".$end."', '".$standort."', '".$error."', '".$warning."')"); + $stmt = $pdo->prepare(" + INSERT INTO togo_termin (date, start, end, standort, error, warning) + VALUES (:date, :start, :ende, :standort, :error, :warning) +"); +$queryinsert = $stmt->execute([ + 'date' => $date, + 'start' => $start, + 'ende' => $end, + 'standort' => $standort, + 'error' => $error, + 'warning' => $warning +]); // Termin wird angelegt if($queryinsert){ @@ -647,8 +706,19 @@ if(!check_worker()){ $mail = $_POST["mail"]; $name = $_POST["name"]; ##Doppelte Anemldung blocken zum gleichen Termin - $queryinsert = mysqli_query($con, "Select * FROM togo_termin_teilnehmer WHERE terminid = '".$terminid."' AND mail = '".$mail."' AND name = '".$name."' AND ip = '".$ip."'"); - $rows = mysqli_num_rows($queryinsert); + // Prepared Statement: $terminid, $mail, $name und $ip kommen direkt + // aus $_POST und wurden vorher ungeprueft in das SQL verkettet. + $stmtDup = $pdo->prepare(" + SELECT COUNT(*) FROM togo_termin_teilnehmer + WHERE terminid = :terminid AND mail = :mail AND name = :name AND ip = :ip + "); + $stmtDup->execute([ + 'terminid' => $terminid, + 'mail' => $mail, + 'name' => $name, + 'ip' => $ip + ]); + $rows = (int)$stmtDup->fetchColumn(); //echo $rows; //echo $rowsvierzehen; @@ -662,8 +732,21 @@ if(!check_worker()){ }else{ - $queryinsert = mysqli_query($con, "UPDATE togo_termin SET count = count + 1 WHERE terminid = '".$terminid."'"); - $queryinserttwo = mysqli_query($con, "INSERT INTO togo_termin_teilnehmer (terminid, ip, mail, create_time,name) VALUES ('".$terminid."', '".$ip."', '".$mail."', '". date("Y-m-d H:i:s") ."', '".$name."')"); + $stmtCount = $pdo->prepare("UPDATE togo_termin SET count = count + 1 WHERE terminid = :terminid"); + $queryinsert = $stmtCount->execute(['terminid' => $terminid]); + + // create_time per NOW() statt per PHP-date(): Webserver und DB-Server + // laufen in unterschiedlichen Zeitzonen. + $stmtTeiln = $pdo->prepare(" + INSERT INTO togo_termin_teilnehmer (terminid, ip, mail, create_time, name) + VALUES (:terminid, :ip, :mail, NOW(), :name) + "); + $queryinserttwo = $stmtTeiln->execute([ + 'terminid' => $terminid, + 'ip' => $ip, + 'mail' => $mail, + 'name' => $name + ]); // Termin wird angelegt if($queryinsert){ diff --git a/inc/functions.inc.php b/inc/functions.inc.php index 52dd015..c37031a 100644 --- a/inc/functions.inc.php +++ b/inc/functions.inc.php @@ -1995,6 +1995,35 @@ function redirectToLogin(): void +/** + * Prueft einmal je Request, ob securitytokens eine Spalte expires_at hat. + * + * Die Spalte wird per Migration nachgeruestet (admin/sql/2026-09-02_securitytokens_expires_at.sql). + * Solange sie fehlt, verhaelt sich check_admin_user() wie bisher; sobald sie da + * ist, greift der Ablauf - ohne dass der Code noch einmal angefasst werden muss. + */ +function securitytokensHatAblaufspalte(PDO $pdo): bool +{ + static $vorhanden = null; + if ($vorhanden !== null) { + return $vorhanden; + } + try { + $stmt = $pdo->prepare(" + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'securitytokens' + AND COLUMN_NAME = 'expires_at' + "); + $stmt->execute(); + $vorhanden = ((int)$stmt->fetchColumn() > 0); + } catch (Throwable $e) { + error_log('securitytokensHatAblaufspalte: ' . $e->getMessage()); + $vorhanden = false; + } + return $vorhanden; +} function check_admin_user() { global $pdo; @@ -2014,7 +2043,12 @@ function check_admin_user() { $identifier = (string)$_COOKIE['identifier']; $securitytoken = (string)$_COOKIE['securitytoken']; - $st = $pdo->prepare("SELECT user_id, securitytoken FROM securitytokens WHERE identifier = ? AND user_type = 'admin' LIMIT 1"); + // Abgelaufene Tokens gar nicht erst laden. Ohne die Spalte gilt das + // alte Verhalten - siehe securitytokensHatAblaufspalte(). + $ablauf = securitytokensHatAblaufspalte($pdo) + ? " AND (expires_at IS NULL OR expires_at > NOW())" + : ""; + $st = $pdo->prepare("SELECT user_id, securitytoken FROM securitytokens WHERE identifier = ? AND user_type = 'admin'" . $ablauf . " LIMIT 1"); $st->execute([$identifier]); $row = $st->fetch(PDO::FETCH_ASSOC); diff --git a/intern/admin.php b/intern/admin.php deleted file mode 100644 index cc7f83f..0000000 --- a/intern/admin.php +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - diff --git a/intern/authentifizierung.php b/intern/authentifizierung.php index e73fc68..534922e 100644 --- a/intern/authentifizierung.php +++ b/intern/authentifizierung.php @@ -15,17 +15,22 @@ $userid = $_GET['userid']; $code = $_GET['code']; //Abfrage des Nutzers -$statement = $pdo->prepare("SELECT * FROM intern_users WHERE id = :userid"); +$statement = $pdo->prepare("SELECT *, (passwortcode_time IS NOT NULL AND passwortcode_time > DATE_SUB(NOW(), INTERVAL 24 HOUR)) AS code_noch_gueltig FROM intern_users WHERE id = :userid"); $result = $statement->execute(array('userid' => $userid)); $user = $statement->fetch(); //Überprüfe dass ein Nutzer gefunden wurde und dieser auch ein Authentifizierungscode angefordert hat -if($user === null || $user['passwortcode'] === null) { +// fetch() liefert false und nicht null, wenn kein Benutzer gefunden wird - +// die alte Bedingung griff daher nie und lief mit false in den Array-Zugriff. +if(!$user || $user['passwortcode'] === null) { error("Der Benutzer wurde nicht gefunden oder hat kein ein Authentifizierungscode angefordert."); $showForm = false; } -if($user['passwortcode_time'] === null || strtotime($user['passwortcode_time']) < (time()-24*3600) ) { +// Ablauf in SQL pruefen: passwortcode_time wird mit NOW() gesetzt (Uhr des +// DB-Servers), waehrend PHP auf dem Webserver in einer anderen Zeitzone +// laeuft. strtotime()/time() lag hier um genau diese Differenz daneben. +if(!$user['code_noch_gueltig']) { error("Dein Code ist leider abgelaufen. Bitte benutze die Authentifizierungsfunktion erneut."); $showForm = false; } @@ -51,7 +56,8 @@ if(check_mailreg()){ }elseif($showForm){ - $passworthash = password_hash($passwort, PASSWORD_DEFAULT); + // (hier stand ein password_hash() auf einer nie gesetzten Variablen, dessen + // Ergebnis niemand benutzt hat - es warf nur eine Deprecation-Warnung) $statement = $pdo->prepare("UPDATE intern_users SET mailreg='1', passwortcode = NULL, passwortcode_time = NULL WHERE id = :userid"); $result = $statement->execute(array('userid'=> $userid )); diff --git a/intern/mailtemplatebetreff.php b/intern/mailtemplatebetreff.php deleted file mode 100644 index aaa44d8..0000000 --- a/intern/mailtemplatebetreff.php +++ /dev/null @@ -1,19 +0,0 @@ - \ No newline at end of file diff --git a/intern/mailtemplatebody.php b/intern/mailtemplatebody.php deleted file mode 100644 index d9d4aab..0000000 --- a/intern/mailtemplatebody.php +++ /dev/null @@ -1,60 +0,0 @@ - \ No newline at end of file diff --git a/intern/meineanfragen.php b/intern/meineanfragen.php index 9f50bb7..4b04cac 100644 --- a/intern/meineanfragen.php +++ b/intern/meineanfragen.php @@ -54,10 +54,25 @@ include("templates/header.inc.php"); if(check_mailreg() && check_userdatenvorhanden() ){ if($_POST["aktion"] == "11"){ - $sqlstring = "SELECT * FROM anfragen INNER JOIN persons ON anfragen.requester_person_id = persons.person_id INNER JOIN anfrageart ON anfragen.anforderungart = anfrageart.artid WHERE anfrageid='" . $_POST["anfrageid"] . "'"; - $query = mysqli_query($con,$sqlstring); + // Zwei Fehler auf einmal: die anfrageid kam ungeprueft aus $_POST in das + // SQL, und gefiltert wurde ausschliesslich danach - jeder angemeldete + // Patient konnte so fremde Anfragen mitsamt Name, Geburtstag, Adresse + // und Telefonnummer abrufen. Jetzt Prepared Statement und zusaetzlich + // an die E-Mail des angemeldeten Benutzers gebunden, wie in der + // Listenansicht weiter unten. + $statementAnfrage = $pdo->prepare(" + SELECT * FROM anfragen + INNER JOIN persons ON anfragen.requester_person_id = persons.person_id + INNER JOIN anfrageart ON anfragen.anforderungart = anfrageart.artid + WHERE anfragen.anfrageid = :anfrageid + AND persons.email = :email + "); + $statementAnfrage->execute([ + 'anfrageid' => (int)($_POST["anfrageid"] ?? 0), + 'email' => $user["email"] + ]); // Ticket und Antwort ansehen. - while ($row = $query->fetch_assoc()) { + while ($row = $statementAnfrage->fetch(PDO::FETCH_ASSOC)) { $anfrageid = $row["anfrageid"];