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>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
@@ -24,7 +23,7 @@ if (!$user) {
|
||||
<?php
|
||||
##test2
|
||||
echo '<div style="float: right; width: 200px; ">';
|
||||
echo "<form action='". $_SERVER['PHP_SELF'] . "' id='formbenutzersuche' method=POST>";
|
||||
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 Anfragen:</label>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . "/../inc/config.inc.php";
|
||||
require_once __DIR__ . "/../inc/functions.inc.php";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . "/../inc/config.inc.php";
|
||||
require_once __DIR__ . "/../inc/functions.inc.php";
|
||||
|
||||
+6
-9
@@ -1,7 +1,4 @@
|
||||
<?php
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
@@ -67,25 +64,25 @@ include("templates/footer.inc.php");
|
||||
echo '<h4>Welche Aktion möchtest du durchführen?</h4>
|
||||
<table width=100%><tr><td width=40%>
|
||||
|
||||
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
|
||||
<form action="'. self_action() .'" method=POST>
|
||||
<input type=hidden name=aktion value=1>
|
||||
<input type=submit class="btn btn-primary btn-lg" value="Formular-Anfragen bearbeiten">
|
||||
</form>
|
||||
</td></tr>
|
||||
<tr><td><br><br>
|
||||
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
|
||||
<form action="'. self_action() .'" method=POST>
|
||||
<input type=hidden name=aktion value=18>
|
||||
<input type=submit class="btn btn-primary" value="Mailvorlagen anlegen">
|
||||
</form>
|
||||
</td></tr>
|
||||
<tr><td><br><br>
|
||||
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
|
||||
<form action="'. self_action() .'" method=POST>
|
||||
<input type=hidden name=aktion value=12>
|
||||
<input type=submit class="btn btn-primary" value="Formular Auswertung">
|
||||
</form>
|
||||
</td></tr>
|
||||
<tr><td><br><br>
|
||||
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
|
||||
<form action="'. self_action() .'" method=POST>
|
||||
<input type=hidden name=aktion value=16>
|
||||
<input type=submit class="btn btn-primary" value="Mailvorlagen anpassen">
|
||||
</form>
|
||||
@@ -93,13 +90,13 @@ include("templates/footer.inc.php");
|
||||
<tr></tr>
|
||||
<tr><td><h3>Urlaub / Hinweis planen</h3></td><td></td></tr>
|
||||
<tr><td>
|
||||
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
|
||||
<form action="'. self_action() .'" method=POST>
|
||||
<input type=hidden name=aktion value=4>
|
||||
<input type=submit class="btn btn-primary btn-lg" value="Urlaub eintragen">
|
||||
</form>
|
||||
</td></tr>
|
||||
<tr><td><br><br>
|
||||
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
|
||||
<form action="'. self_action() .'" method=POST>
|
||||
<input type=hidden name=aktion value=6>
|
||||
<input type=submit class="btn btn-primary" value="Notfallsprechstunde eintragen">
|
||||
</form>
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
@@ -55,7 +54,7 @@ function build_calendar($month, $year) {
|
||||
$calendar .= "<td class='day' rel='$date' onClick='submitForm(\"".$date ."\")'>$currentDay";
|
||||
|
||||
}
|
||||
$calendar .= "<form action='". $_SERVER['PHP_SELF'] . "' name='".$date ."' method=POST><input type=hidden name=searchdate value='". $date. "'><input type=hidden name=aktion value='4'></form>";
|
||||
$calendar .= "<form action='". self_action() . "' name='".$date ."' method=POST><input type=hidden name=searchdate value='". $date. "'><input type=hidden name=aktion value='4'></form>";
|
||||
$calendar .= AuswertungImpfungdailycalendar($date);
|
||||
$calendar .= "</td>";
|
||||
|
||||
|
||||
+66
-56
@@ -1,9 +1,6 @@
|
||||
<?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");
|
||||
@@ -14,61 +11,74 @@ 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);
|
||||
// Brute-Force-Bremse: nach mehreren Fehlversuchen ist die Anmeldung fuer
|
||||
// dieses Konto bzw. diese IP zeitweise gesperrt. Die Pruefung laeuft vor
|
||||
// password_verify(), damit gesperrte Versuche gar nicht erst rechnen.
|
||||
$sperreSekunden = login_gesperrt($pdo, $email);
|
||||
|
||||
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;
|
||||
if ($sperreSekunden > 0) {
|
||||
$error_msg = login_sperr_text($sperreSekunden);
|
||||
} else {
|
||||
$error_msg = "E-Mail oder Passwort war ungültig<br><br>";
|
||||
|
||||
$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'])) {
|
||||
|
||||
login_versuch_merken($pdo, $email, true);
|
||||
|
||||
// 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 {
|
||||
login_versuch_merken($pdo, $email, false);
|
||||
$error_msg = "E-Mail oder Passwort war ungültig<br><br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-9
@@ -1,15 +1,24 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
unset($_SESSION['userid']);
|
||||
|
||||
//Remove Cookies
|
||||
setcookie("identifier","",time()-(3600*24*365));
|
||||
setcookie("securitytoken","",time()-(3600*24*365));
|
||||
|
||||
<?php
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
// Die Sitzung wird von config.inc.php mit den richtigen Cookie-Parametern
|
||||
// gestartet, deshalb erst danach abraeumen.
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
|
||||
// Cookies der Funktion "Angemeldet bleiben" entfernen. Die Parameter muessen
|
||||
// denen beim Setzen entsprechen, sonst bleibt das Cookie stehen.
|
||||
$cookieOpts = [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
];
|
||||
setcookie('identifier', '', $cookieOpts);
|
||||
setcookie('securitytoken', '', $cookieOpts);
|
||||
|
||||
include("templates/header.inc.php");
|
||||
?>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
include("templates/header.inc.php");
|
||||
@@ -44,7 +43,7 @@ dein IT Team';
|
||||
|
||||
//echo $text;
|
||||
|
||||
$ok = SendMailMessage($pdo, $mail, $betreff, $body);
|
||||
$ok = SendMailMessage($pdo, $empfaenger, $betreff, $text);
|
||||
|
||||
if ($ok) {
|
||||
echo "<b>Ihre Nachricht wurde erfolgreich versendet!</b><br>";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
if(!isset($_GET['userid']) || !isset($_GET['code'])) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// WICHTIG: Pfade aus /admin heraus korrekt auflösen
|
||||
require_once __DIR__ . "/../inc/config.inc.php";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Brute-Force-Bremse fuer die Anmeldung.
|
||||
--
|
||||
-- Haelt fehlgeschlagene und erfolgreiche Anmeldeversuche fest, damit
|
||||
-- login_gesperrt() in inc/security.inc.php nach mehreren Fehlversuchen
|
||||
-- zeitweise sperren kann. Ohne diese Tabelle laeuft der Login unveraendert
|
||||
-- weiter, nur eben ohne Bremse.
|
||||
--
|
||||
-- Aufraeumen passiert im Code (Eintraege aelter als 30 Tage).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS login_attempts (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
erfolg TINYINT(1) NOT NULL DEFAULT 0,
|
||||
attempted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_email_zeit (email, attempted_at),
|
||||
KEY idx_ip_zeit (ip_address, attempted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// WICHTIG: Pfade aus /admin heraus korrekt auflösen
|
||||
require_once __DIR__ . "/../inc/config.inc.php";
|
||||
@@ -24,7 +23,7 @@ include("templates/footer.inc.php");
|
||||
<?php
|
||||
|
||||
echo '<div style="float: right; width: 200px; ">';
|
||||
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' id='formbenutzersuche' method='POST'>";
|
||||
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>
|
||||
@@ -111,7 +110,7 @@ if (!check_worker()) {
|
||||
echo "<h4>Vorlage: " . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "</h4>";
|
||||
echo "<br><br>";
|
||||
|
||||
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>";
|
||||
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>";
|
||||
@@ -128,7 +127,7 @@ if (!check_worker()) {
|
||||
echo "</form>";
|
||||
|
||||
echo "<br><br>";
|
||||
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>";
|
||||
echo "<form action='" . self_action() . "' method='POST'>";
|
||||
echo "<input type='submit' class='btn btn-primary btn-sm' value='zurück'>";
|
||||
echo "</form>";
|
||||
}
|
||||
@@ -144,7 +143,7 @@ if (!check_worker()) {
|
||||
// =========================
|
||||
} else {
|
||||
|
||||
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>";
|
||||
echo "<form action='" . self_action() . "' method='POST'>";
|
||||
echo "Wählen Sie die zu bearbeitenden Webseiteninhalt aus:<br><br>";
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . "/../inc/config.inc.php";
|
||||
require_once __DIR__ . "/../inc/functions.inc.php";
|
||||
|
||||
+16
-1
@@ -12,6 +12,8 @@ if (!is_readable($credentials)) {
|
||||
}
|
||||
require $credentials; // liefert $dbhost, $dbuser, $dbpassword, $dbname
|
||||
|
||||
require_once __DIR__ . '/security.inc.php';
|
||||
|
||||
|
||||
$organisationsname = "Praxis Creutzburg";
|
||||
$organisationsurl = "https://www.praxis-creutzburg.de/";
|
||||
@@ -79,4 +81,17 @@ ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.cookie_secure', '1'); // bei HTTPS
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
|
||||
session_start();
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// CSRF-Schutz im Adminbereich: Token in jedes POST-Formular einsetzen und
|
||||
// eingehende POSTs pruefen. Muss nach session_start() und vor jeder
|
||||
// Ausgabe der Seite laufen.
|
||||
$adminVerzeichnis = realpath(__DIR__ . '/../admin');
|
||||
$laufendesSkript = realpath($_SERVER['SCRIPT_FILENAME'] ?? '');
|
||||
if ($adminVerzeichnis !== false && $laufendesSkript !== false
|
||||
&& strncmp($laufendesSkript, $adminVerzeichnis . DIRECTORY_SEPARATOR, strlen($adminVerzeichnis) + 1) === 0) {
|
||||
csrf_autoinject();
|
||||
csrf_require();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
/**
|
||||
* Sicherheitsfunktionen: CSRF-Schutz, Brute-Force-Bremse, sichere Formularziele.
|
||||
*
|
||||
* Wird von inc/config.inc.php eingebunden und steht damit ueberall zur
|
||||
* Verfuegung, wo auch $pdo verfuegbar ist.
|
||||
*/
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CSRF
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Liefert das CSRF-Token der aktuellen Sitzung und legt es beim ersten
|
||||
* Aufruf an.
|
||||
*/
|
||||
function csrf_token(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fertiges Hidden-Feld fuer Formulare.
|
||||
*/
|
||||
function csrf_field(): string
|
||||
{
|
||||
return '<input type="hidden" name="csrf_token" value="'
|
||||
. htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8') . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft das mitgeschickte Token. Akzeptiert das Formularfeld und den
|
||||
* Header X-CSRF-Token, damit auch AJAX-Aufrufe funktionieren.
|
||||
*/
|
||||
function csrf_valid(): bool
|
||||
{
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
|
||||
if (!is_string($token) || $token === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bricht jeden POST ohne gueltiges Token ab. Fuer GET-Anfragen wirkungslos.
|
||||
*/
|
||||
function csrf_require(): void
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
return;
|
||||
}
|
||||
if (csrf_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code(403);
|
||||
|
||||
$istAjax = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest';
|
||||
if ($istAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Sicherheitstoken ungueltig. Bitte neu anmelden.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo '<h3>Die Anfrage wurde abgelehnt.</h3>';
|
||||
echo 'Das Sicherheitstoken war ungültig oder die Sitzung ist abgelaufen.<br>';
|
||||
echo 'Bitte die Seite neu laden und den Vorgang wiederholen.<br><br>';
|
||||
echo '<a href="index.php">Zurück zum Adminbereich</a>';
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Haengt das Token an jedes POST-Formular der Ausgabe. Wird als Callback von
|
||||
* ob_start() benutzt, damit die rund 60 bestehenden Formulare nicht einzeln
|
||||
* angefasst werden muessen.
|
||||
*/
|
||||
function csrf_inject_output(string $html): string
|
||||
{
|
||||
$ersetzt = preg_replace_callback(
|
||||
'/<form\b[^>]*>/i',
|
||||
static function (array $treffer): string {
|
||||
$tag = $treffer[0];
|
||||
// Nur POST-Formulare brauchen ein Token.
|
||||
if (!preg_match('/method\s*=\s*["\']?\s*post/i', $tag)) {
|
||||
return $tag;
|
||||
}
|
||||
return $tag . csrf_field();
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
// preg_replace_callback liefert bei einem Fehler null - dann lieber die
|
||||
// unveraenderte Seite ausliefern als eine leere.
|
||||
return $ersetzt ?? $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert das automatische Einfuegen. Muss vor jeder Ausgabe laufen.
|
||||
*/
|
||||
function csrf_autoinject(): void
|
||||
{
|
||||
ob_start('csrf_inject_output');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sicheres Ziel fuer Formulare und Links auf die eigene Seite.
|
||||
*
|
||||
* Ersetzt $_SERVER['PHP_SELF']: das enthaelt bei Aufrufen wie
|
||||
* /admin/anfragen.php/"><script> auch den angehaengten Pfad und landete
|
||||
* ungeprueft im HTML.
|
||||
*/
|
||||
function self_action(): string
|
||||
{
|
||||
return htmlspecialchars(basename($_SERVER['SCRIPT_NAME'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brute-Force-Bremse fuer Anmeldungen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LOGIN_MAX_VERSUCHE_KONTO = 5; // pro E-Mail-Adresse
|
||||
const LOGIN_MAX_VERSUCHE_IP = 20; // pro IP-Adresse
|
||||
const LOGIN_ZEITFENSTER_MIN = 15; // Minuten
|
||||
|
||||
/**
|
||||
* Existiert die Tabelle login_attempts? Ohne sie laeuft der Login wie bisher
|
||||
* weiter - gleiche Vorgehensweise wie bei securitytokensHatAblaufspalte().
|
||||
*/
|
||||
function loginAttemptsTabelleVorhanden(PDO $pdo): bool
|
||||
{
|
||||
static $vorhanden = null;
|
||||
if ($vorhanden !== null) {
|
||||
return $vorhanden;
|
||||
}
|
||||
|
||||
try {
|
||||
$st = $pdo->query("SHOW TABLES LIKE 'login_attempts'");
|
||||
$vorhanden = (bool)$st->fetchColumn();
|
||||
} catch (Throwable $e) {
|
||||
$vorhanden = false;
|
||||
}
|
||||
|
||||
return $vorhanden;
|
||||
}
|
||||
|
||||
function login_ip(): string
|
||||
{
|
||||
return substr((string)($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist die Anmeldung gerade gesperrt? Liefert die Restdauer in Sekunden,
|
||||
* sonst 0.
|
||||
*/
|
||||
function login_gesperrt(PDO $pdo, string $email): int
|
||||
{
|
||||
if (!loginAttemptsTabelleVorhanden($pdo)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$st = $pdo->prepare("
|
||||
SELECT
|
||||
SUM(email = :email) AS konto,
|
||||
SUM(ip_address = :ip) AS ip,
|
||||
MAX(attempted_at) AS letzter
|
||||
FROM login_attempts
|
||||
WHERE erfolg = 0
|
||||
AND attempted_at > DATE_SUB(NOW(), INTERVAL :fenster MINUTE)
|
||||
AND (email = :email2 OR ip_address = :ip2)
|
||||
");
|
||||
$st->bindValue(':email', $email);
|
||||
$st->bindValue(':email2', $email);
|
||||
$st->bindValue(':ip', login_ip());
|
||||
$st->bindValue(':ip2', login_ip());
|
||||
$st->bindValue(':fenster', LOGIN_ZEITFENSTER_MIN, PDO::PARAM_INT);
|
||||
$st->execute();
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row || $row['letzter'] === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ueberschritten = (int)$row['konto'] >= LOGIN_MAX_VERSUCHE_KONTO
|
||||
|| (int)$row['ip'] >= LOGIN_MAX_VERSUCHE_IP;
|
||||
if (!$ueberschritten) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sperre laeuft aus, sobald der letzte Fehlversuch aus dem Zeitfenster
|
||||
// faellt. Zeitrechnung in SQL, weil Web- und DB-Server in
|
||||
// unterschiedlichen Zeitzonen laufen.
|
||||
$st = $pdo->prepare("
|
||||
SELECT GREATEST(0, TIMESTAMPDIFF(SECOND, NOW(),
|
||||
DATE_ADD(:letzter, INTERVAL :fenster MINUTE)))
|
||||
");
|
||||
$st->bindValue(':letzter', $row['letzter']);
|
||||
$st->bindValue(':fenster', LOGIN_ZEITFENSTER_MIN, PDO::PARAM_INT);
|
||||
$st->execute();
|
||||
|
||||
return (int)$st->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Haelt einen Anmeldeversuch fest.
|
||||
*/
|
||||
function login_versuch_merken(PDO $pdo, string $email, bool $erfolg): void
|
||||
{
|
||||
if (!loginAttemptsTabelleVorhanden($pdo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$st = $pdo->prepare("
|
||||
INSERT INTO login_attempts (email, ip_address, erfolg)
|
||||
VALUES (:email, :ip, :erfolg)
|
||||
");
|
||||
$st->execute([
|
||||
':email' => substr($email, 0, 255),
|
||||
':ip' => login_ip(),
|
||||
':erfolg' => $erfolg ? 1 : 0,
|
||||
]);
|
||||
|
||||
if ($erfolg) {
|
||||
// Nach erfolgreicher Anmeldung ist die Bremse fuer dieses Konto
|
||||
// wieder offen.
|
||||
$del = $pdo->prepare("DELETE FROM login_attempts WHERE email = :email AND erfolg = 0");
|
||||
$del->execute([':email' => $email]);
|
||||
}
|
||||
|
||||
// Alte Eintraege gelegentlich aufraeumen.
|
||||
if (random_int(1, 50) === 1) {
|
||||
$pdo->exec("DELETE FROM login_attempts WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 30 DAY)");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('login_versuch_merken: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einheitlicher Text fuer eine aktive Sperre.
|
||||
*/
|
||||
function login_sperr_text(int $sekunden): string
|
||||
{
|
||||
$minuten = (int)ceil($sekunden / 60);
|
||||
return 'Zu viele fehlgeschlagene Anmeldeversuche. '
|
||||
. 'Bitte in ' . $minuten . ' Minute' . ($minuten === 1 ? '' : 'n') . ' erneut versuchen.<br><br>';
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
include("templates/header.inc.php");
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Ausgabe puffern: check_intern_user() setzt Cookies und leitet per header() um,
|
||||
// obwohl das Header-Template weiter unten schon Ausgabe erzeugt hat.
|
||||
ob_start();
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
ob_start();
|
||||
session_start();
|
||||
require_once(__DIR__ . "/../inc/config.inc.php");
|
||||
require_once(__DIR__ . "/../inc/functions.inc.php");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user