Files
praxis-creutzburg-web/admin/webseitenadmin.php
T
clemensandClaude Opus 5 19f1ac7ec8 CSRF-Schutz, Brute-Force-Bremse und wirksame Session-Cookie-Flags
Neu: inc/security.inc.php, eingebunden von inc/config.inc.php.

CSRF
Der Adminbereich hatte keinerlei Schutz gegen fremde POSTs. Jetzt haengt
csrf_inject_output() als Ausgabefilter das Token an jedes POST-Formular und
csrf_require() weist POSTs ohne gueltiges Token ab. Beides wird nur fuer
Skripte unterhalb von admin/ aktiviert, der oeffentliche Bereich bleibt
unveraendert. Der Umweg ueber den Ausgabefilter erspart es, die rund 60
bestehenden Formulare einzeln anzufassen; der AJAX-Aufruf auf
mailtemplate.php schickt das Token als Feld mit.

Session-Cookie-Flags
config.inc.php setzt secure, httponly und samesite - aber 25 Dateien in
admin/ und intern/ riefen session_start() vor dem Include auf, womit die
Parameter wirkungslos waren. Die vorgezogenen Aufrufe sind entfernt,
config.inc.php startet die Sitzung nur noch, wenn keine laeuft.
admin/logout.php musste umgestellt werden, weil dort session_destroy()
vor dem Include stand; die Cookies werden jetzt mit denselben Parametern
geloescht, mit denen sie gesetzt wurden.

Brute-Force
Nach 5 Fehlversuchen je Konto oder 20 je IP ist die Anmeldung 15 Minuten
gesperrt, gezaehlt in der neuen Tabelle login_attempts. Fehlt die Tabelle,
laeuft der Login wie bisher - gleiche Vorgehensweise wie bei
securitytokensHatAblaufspalte(). Eine erfolgreiche Anmeldung raeumt die
Fehlversuche des Kontos ab.

Passwort vergessen
admin/passwortvergessen.php uebergab $mail und $body an SendMailMessage();
beide Variablen gibt es dort nicht, sie heissen $empfaenger und $text. Die
Mail ging deshalb nie raus, obwohl der Reset-Code gesetzt wurde.

Getestet gegen einen lokalen PHP-Server: Token wird eingesetzt, POST ohne
Token liefert 403, mit Token laeuft der Login normal, der sechste
Fehlversuch wird gesperrt, das Sitzungscookie traegt secure/HttpOnly/
SameSite. Oeffentliche Seiten sind unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:18:59 +02:00

182 lines
7.2 KiB
PHP

<?php
// WICHTIG: Pfade aus /admin heraus korrekt auflösen
require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php";
// Login prüfen
$user = check_admin_user();
include __DIR__ . "/templates/header.inc.php";
$user = check_admin_user();
$internUserId = (int)$_SESSION['auth']['id'];
if (!$user) { echo "<div class='container main-container'><h3>Erst anmelden: <a href=login.php>Login</a></h3><br>";
include("templates/footer.inc.php");
exit; }
?>
<script src="js/tinymce/tinymce.min.js" referrerpolicy="origin"></script>
<div class="container main-container">
<?php
echo '<div style="float: right; width: 200px; ">';
echo "<form action='" . self_action() . "' id='formbenutzersuche' method='POST'>";
echo '<input type="hidden" name="aktion" value="benutzersuche" />';
echo '<input type="hidden" name="userid_input" id="userid_input" />';
echo '<label>Benutzersuche Impfen:</label>
<input type="text" id="user_input" name="skill_input" width="48"/>';
echo "</form>";
echo '</div>';
?>
<h2>Administration - webseiteninhalt anpassen</h2>
Hallo <?php echo htmlspecialchars($user['vorname'] ?? '', ENT_QUOTES, 'UTF-8'); ?>,<br>
Herzlich Willkommen im internen Bereich!<br><br>
<?php
$aktion = $_POST["aktion"] ?? null;
$subaktion = $_POST["subaktion"] ?? null;
if (!check_worker()) {
echo "Dieser Bereich ist nur für Bearbeiter freigeschaltet!<br><br><br>";
} else {
// =========================
// EDIT-MODUS
// =========================
if ($aktion === "edit") {
// ------- Speichern -------
if ($subaktion === "save") {
$inhaltid = (int)($_POST["inhaltid"] ?? 0);
$inhalt = $_POST["inhalt"] ?? "";
$webseitentitel = $_POST["webseitentitel"] ?? "";
$beschreibung = $_POST["beschreibung"] ?? "";
$url = $_POST["url"] ?? "";
try {
$stmt = $pdo->prepare("
UPDATE webseiteninhalt
SET inhalt = :inhalt,
webseitentitel = :webseitentitel,
beschreibung = :beschreibung,
url = :url
WHERE inhaltid = :inhaltid
");
$stmt->execute([
':inhalt' => $inhalt,
':webseitentitel' => $webseitentitel,
':beschreibung' => $beschreibung,
':url' => $url,
':inhaltid' => $inhaltid,
]);
echo "<div class='infofenster'><h4>Speicherung der Vorlage erfolgreich!</h4></div>";
} catch (Throwable $e) {
echo "<div class='infofenster'><h4>Fehler bei der Speicherung der Vorlage!</h4></div>";
// Optional debug:
// echo "<pre>" . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . "</pre>";
}
}
// ------- Vorlage laden -------
$inhaltid = (int)($_POST["inhaltid"] ?? 0);
try {
$stmt = $pdo->prepare("
SELECT webseitentitel, inhalt, beschreibung, url
FROM webseiteninhalt
WHERE inhaltid = ?
LIMIT 1
");
$stmt->execute([$inhaltid]);
$rowconfig = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$rowconfig) {
echo "<div class='infofenster'><h4>Vorlage nicht gefunden.</h4></div>";
} else {
$webseitentitel = $rowconfig["webseitentitel"] ?? "";
$inhalt = $rowconfig["inhalt"] ?? "";
$beschreibung = $rowconfig["beschreibung"] ?? "";
$url = $rowconfig["url"] ?? "";
echo "<h1>Webseiteninhalt bearbeiten</h1><br>";
echo "<h4>Vorlage: " . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "</h4>";
echo "<br><br>";
echo "<form action='" . self_action() . "' method='POST'>";
echo "<label>Titel</label><br>";
echo "<input name='webseitentitel' type='text' class='form-control' value='" . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "'><br><br>";
echo "<label>Beschreibung</label><br>";
echo "<input name='beschreibung' type='text' class='form-control' value='" . htmlspecialchars($beschreibung, ENT_QUOTES, 'UTF-8') . "'><br><br>";
echo "<label>URL / Hinweis</label><br>";
echo "<input name='url' type='text' class='form-control' value='" . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . "'><br><br>";
echo "<div id='my-editor'></div>";
// Inhalt ist HTML -> bewusst NICHT escapen, sonst zerstörst du HTML im Editor
echo "<textarea height='200' name='inhalt' id='trumbowyg-demo'>" . $inhalt . "</textarea>";
echo "<input name='aktion' type='hidden' value='edit'>";
echo "<input name='subaktion' type='hidden' value='save'>";
echo "<input name='inhaltid' type='hidden' value='" . (int)$inhaltid . "'><br><br>";
echo "<input type='submit' value='Speichern'><br>";
echo "</form>";
echo "<br><br>";
echo "<form action='" . self_action() . "' method='POST'>";
echo "<input type='submit' class='btn btn-primary btn-sm' value='zurück'>";
echo "</form>";
}
} catch (Throwable $e) {
echo "<div class='infofenster'><h4>Fehler beim Laden der Vorlage.</h4></div>";
// Optional debug:
// echo "<pre>" . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . "</pre>";
}
// =========================
// LISTE (Auswahl)
// =========================
} else {
echo "<form action='" . self_action() . "' method='POST'>";
echo "Wählen Sie die zu bearbeitenden Webseiteninhalt aus:<br><br>";
try {
$stmt = $pdo->prepare("
SELECT webseitentitel, inhaltid
FROM webseiteninhalt
ORDER BY webseitentitel
");
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "<input name='aktion' type='hidden' value='edit'>";
echo "<select name='inhaltid' class='form-control'>";
foreach ($rows as $row) {
$id = (int)$row["inhaltid"];
$titel = (string)($row["webseitentitel"] ?? "");
echo "<option value='" . $id . "'>" . htmlspecialchars($titel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</option>";
}
echo "</select><br><br>";
echo "<input type='submit' value='Bearbeiten' class='btn btn-primary btn'><br><br><br>";
echo "</form>";
} catch (Throwable $e) {
echo "<div class='infofenster'><h4>Fehler beim Laden der Liste.</h4></div>";
// Optional debug:
// echo "<pre>" . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . "</pre>";
}
}
}
include __DIR__ . "/templates/footer.inc.php";
?>
</div>