CSRF - erste Erichtigung

This commit is contained in:
2026-07-12 08:55:56 +02:00
parent 174fff6f31
commit 444f0a269b
6 changed files with 75 additions and 32 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ Testing, markiert aber die wichtigsten Risiken fuer die SaaS-Umstrukturierung.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Hart codierte DB-Zugangsdaten | `jahresauswertung.php` Zeilen 4-8 | Secret-Leak, direkte Produktiv-DB-Gefahr | Zugangsdaten rotieren, Skript deaktivieren oder auf Env-Konfiguration umstellen | | Hart codierte DB-Zugangsdaten | `jahresauswertung.php` Zeilen 4-8 | Secret-Leak, direkte Produktiv-DB-Gefahr | Zugangsdaten rotieren, Skript deaktivieren oder auf Env-Konfiguration umstellen |
| Schreibende Seiten ohne eigene Rollenpruefung | `stricheintragen.php`, `einzahlung.php`, `mailversenden.php`, `exportKaffeeliste.php` | Direkter URL-Aufruf kann Aktionen erlauben | Jede Seite serverseitig mit `requireRole` absichern | | Schreibende Seiten ohne eigene Rollenpruefung | `stricheintragen.php`, `einzahlung.php`, `mailversenden.php`, `exportKaffeeliste.php` | Direkter URL-Aufruf kann Aktionen erlauben | Jede Seite serverseitig mit `requireRole` absichern |
| Keine CSRF-Token | nahezu alle POST-/Delete-Formulare | Ungewollte Buchungen, Loeschungen, Imports | CSRF fuer alle schreibenden Aktionen | | CSRF-Schutz nur teilweise vorhanden | viele POST-/Delete-Formulare; M2 hat `hinweise.php`, `mitarbeiterverwalten.php`, `namenanpassen.php` abgesichert | Ungewollte Buchungen, Loeschungen, Imports | CSRF fuer alle verbleibenden schreibenden Aktionen |
| Harte Deletes fuer Buchungen | `letzteneintraege.php` | Audit-Historie und Revisionsfaehigkeit gehen verloren | Storno-/Reversal-Modell statt Delete | | Harte Deletes fuer Buchungen | `letzteneintraege.php` | Audit-Historie und Revisionsfaehigkeit gehen verloren | Storno-/Reversal-Modell statt Delete |
| Upload in Webroot | `csvupload.php` Zeilen 131-138 | Dateiablage kann missbraucht werden | Upload ausserhalb Webroot, Dateityp/MIME/Name pruefen | | Upload in Webroot | `csvupload.php` Zeilen 131-138 | Dateiablage kann missbraucht werden | Upload ausserhalb Webroot, Dateityp/MIME/Name pruefen |
| Kein Tenant-Scope | alle fachlichen Queries | Zentrales SaaS-Leak-Risiko | `tenant_id` verpflichtend und Query-Schicht testen | | Kein Tenant-Scope | alle fachlichen Queries | Zentrales SaaS-Leak-Risiko | `tenant_id` verpflichtend und Query-Schicht testen |
@@ -52,7 +52,7 @@ Fuer SaaS gilt:
1. Secrets rotieren und aus dem Code entfernen. 1. Secrets rotieren und aus dem Code entfernen.
2. Legacy-Schreibseiten bis zum Umbau hinter explizite Admin-Pruefung setzen. 2. Legacy-Schreibseiten bis zum Umbau hinter explizite Admin-Pruefung setzen.
3. CSRF-Schutz in der neuen App-Basis einplanen. 3. CSRF-Schutz schrittweise auf alle verbleibenden Legacy-Schreibseiten ausrollen.
4. Finanzdaten im Zielmodell nur noch stornieren, nicht loeschen. 4. Finanzdaten im Zielmodell nur noch stornieren, nicht loeschen.
5. Uploads ausserhalb des Webroots modellieren. 5. Uploads ausserhalb des Webroots modellieren.
6. Einheitliche Escape-/View-Helfer einfuehren. 6. Einheitliche Escape-/View-Helfer einfuehren.
+30 -6
View File
@@ -35,6 +35,23 @@ Legacy-Verhalten oder das bestehende Design zu veraendern.
werden spaeter einzeln umgestellt, damit keine Formulare oder Spezialflows werden spaeter einzeln umgestellt, damit keine Formulare oder Spezialflows
brechen. brechen.
### CSRF-Rollout
Erste Legacy-POST-Seiten sind opt-in abgesichert:
- `hinweise.php`: Hinweis anlegen und Hinweis loeschen. Der bisherige
GET-Loeschlink wurde durch ein POST-Formular mit CSRF-Token ersetzt.
- `mitarbeiterverwalten.php`: Mitglied anlegen, Bearbeitungsformular oeffnen,
Mitglied speichern, aktivieren und deaktivieren.
- `namenanpassen.php`: Anzeigenamen aktualisieren.
Noch offen:
- Buchungsflows: `index.php`, `stricheintragen.php`, `einzahlung.php`.
- Korrektur-/Loeschflows: `letzteneintraege.php`.
- Uploads: `csvupload.php`.
- Spezialprozesse: `mailversenden.php`, `jahresauswertung.php`.
### Migrationen ### Migrationen
- `database/migrations/0001_legacy_mysql_baseline.sql` bildet die bisherige - `database/migrations/0001_legacy_mysql_baseline.sql` bildet die bisherige
@@ -82,21 +99,28 @@ Die Skripte erwarten die bekannten Dev-Umgebungsvariablen `DB_HOST`, `DB_NAME`,
- PHP-Syntax fuer Bootstrap, Migrationen, Init-Skript und Config ist sauber. - PHP-Syntax fuer Bootstrap, Migrationen, Init-Skript und Config ist sauber.
- Session-Start laeuft in der lokalen Dev-Umgebung ohne PHP-Warnings ueber - Session-Start laeuft in der lokalen Dev-Umgebung ohne PHP-Warnings ueber
`var/sessions`. `var/sessions`.
- CSRF negative Tests: `hinweise.php`, `mitarbeiterverwalten.php` und
`namenanpassen.php` liefern bei POST ohne Token HTTP 419.
- CSRF positive Tests: gueltige Token funktionieren fuer Hinweis-Anlage,
Mitglieder-Bearbeitungsformular und Namensanpassung. Der temporaere
Testhinweis wurde wieder entfernt.
- Golden Master weiterhin gruen mit 104 Assertions. - Golden Master weiterhin gruen mit 104 Assertions.
- HTTP-Smoke weiterhin gruen mit 14 sicheren Seiten. - HTTP-Smoke weiterhin gruen mit 14 sicheren Seiten.
## Bewusste Grenzen ## Bewusste Grenzen
- Keine Tenant-/User-/Rollen-Tabellen in M2. Diese gehoeren zu M3. - Keine Tenant-/User-/Rollen-Tabellen in M2. Diese gehoeren zu M3.
- Keine globale CSRF-Erzwingung in M2. Die Absicherung der POST-Seiten erfolgt - Keine globale CSRF-Erzwingung in M2. Die Absicherung weiterer POST-Seiten
schrittweise. erfolgt schrittweise.
- Kein Layout-Umbau in M2. Die visuelle Struktur bleibt stabil. - Kein Layout-Umbau in M2. Die visuelle Struktur bleibt stabil.
- PDF-/Mail-/Jahresprozesse bleiben als M6-Themen offen. - PDF-/Mail-/Jahresprozesse bleiben als M6-Themen offen.
## Naechste Schritte ## Naechste Schritte
1. POST-Seiten einzeln mit `app_csrf_field()` und `app_require_csrf()` 1. Buchungs- und Korrektur-POST-Seiten einzeln mit `app_csrf_field()` und
absichern. `app_require_csrf()` absichern.
2. Eine duenne View-/Layout-Struktur vorbereiten, ohne Header/Footer-Markup 2. CSV-Upload gesondert absichern und spaeter Uploads ausserhalb des Webroots
verlegen.
3. Eine duenne View-/Layout-Struktur vorbereiten, ohne Header/Footer-Markup
sofort zu verschieben. sofort zu verschieben.
3. Danach M3 starten: Tenants, User, Registrierung, Login und Rollen. 4. Danach M3 starten: Tenants, User, Registrierung, Login und Rollen.
+3 -1
View File
@@ -381,7 +381,9 @@ Schritte:
bleiben vorerst Legacy-Wrapper. bleiben vorerst Legacy-Wrapper.
- Bestehende Assets weiterverwenden. - Bestehende Assets weiterverwenden.
- CSRF- und Session-Basis einziehen. Session und CSRF-Helper sind vorhanden; - CSRF- und Session-Basis einziehen. Session und CSRF-Helper sind vorhanden;
globale Erzwingung erfolgt schrittweise pro POST-Seite. globale Erzwingung erfolgt schrittweise pro POST-Seite. Erste Seiten sind
abgesichert: `hinweise.php`, `mitarbeiterverwalten.php`,
`namenanpassen.php`.
- Konfigurationswerte aus Code in Umgebung oder Settings verschieben. - Konfigurationswerte aus Code in Umgebung oder Settings verschieben.
Ergebnis: Ergebnis:
+31 -22
View File
@@ -1,6 +1,7 @@
<?php <?php
include "functions.php"; include "functions.php";
app_require_csrf();
include "header.php"; include "header.php";
include "headerline.php"; include "headerline.php";
include "nav.php"; include "nav.php";
@@ -20,33 +21,34 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
echo "<h2>Kaffeeliste - Hinweise</h2>"; echo "<h2>Kaffeeliste - Hinweise</h2>";
// Hinweis löschen // Hinweis speichern oder löschen
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
$stmt = sqlsrv_query($conn, "DELETE FROM kl_hinweise WHERE id = ?", [$id]);
}
// Hinweis speichern
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nachricht = $_POST['nachricht']; $aktion = $_POST['aktion'] ?? 'speichern';
$gueltig_bis = $_POST['gueltig_bis']; // z.B. "2025-09-03T14:00"
$dt = DateTime::createFromFormat('Y-m-d\TH:i', $gueltig_bis);
if ($dt) { if ($aktion === 'loeschen') {
$gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00" $id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
$stmt = sqlsrv_query($conn, "DELETE FROM kl_hinweise WHERE id = ?", [$id]);
}
} else { } else {
die("Ungültiges Datumsformat"); $nachricht = $_POST['nachricht'];
} $gueltig_bis = $_POST['gueltig_bis']; // z.B. "2025-09-03T14:00"
$dt = DateTime::createFromFormat('Y-m-d\TH:i', $gueltig_bis);
if (!empty($nachricht) && !empty($gueltig_bis_sql)) { if ($dt) {
$gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00"
} else {
die("Ungültiges Datumsformat");
}
if (!empty($nachricht) && !empty($gueltig_bis_sql)) {
$stmt = sqlsrv_query($conn, $stmt = sqlsrv_query($conn,
"INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, ?)", "INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, ?)",
[$nachricht, $gueltig_bis_sql] [$nachricht, $gueltig_bis_sql]
); );
}
} }
} }
@@ -61,6 +63,8 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
<h2>Neuen Hinweis hinzufügen</h2> <h2>Neuen Hinweis hinzufügen</h2>
<form method="post"> <form method="post">
<input type="hidden" name="aktion" value="speichern">
<?php echo app_csrf_field(); ?>
<label>Nachricht:</label><br> <label>Nachricht:</label><br>
<textarea name="nachricht" required></textarea><br><br> <textarea name="nachricht" required></textarea><br><br>
<label>Gültig bis:</label><br> <label>Gültig bis:</label><br>
@@ -73,7 +77,12 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
<div class="hinweis"> <div class="hinweis">
<strong><?= htmlspecialchars($hinweis['nachricht']) ?></strong><br> <strong><?= htmlspecialchars($hinweis['nachricht']) ?></strong><br>
<small>Gültig bis: <?= $hinweis['gueltig_bis']->format('d.m.Y H:i') ?></small><br> <small>Gültig bis: <?= $hinweis['gueltig_bis']->format('d.m.Y H:i') ?></small><br>
<a href="?delete=<?= $hinweis['id'] ?>" onclick="return confirm('Diesen Hinweis wirklich löschen?')">🗑️ Löschen</a> <form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" onsubmit="return confirm('Diesen Hinweis wirklich löschen?')">
<input type="hidden" name="aktion" value="loeschen">
<input type="hidden" name="id" value="<?= (int)$hinweis['id'] ?>">
<?php echo app_csrf_field(); ?>
<button type="submit">Löschen</button>
</form>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</body> </body>
+6
View File
@@ -2,6 +2,7 @@
include "functions.php"; include "functions.php";
app_require_csrf();
include "header.php"; include "header.php";
include "headerline.php"; include "headerline.php";
include "nav.php"; include "nav.php";
@@ -69,6 +70,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>"> <form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<input type="hidden" name="aktion" value="bearbeitenspeichern"> <input type="hidden" name="aktion" value="bearbeitenspeichern">
<input type="hidden" name="mitgliedID" value="<?php echo $mitgliedID; ?>"> <input type="hidden" name="mitgliedID" value="<?php echo $mitgliedID; ?>">
<?php echo app_csrf_field(); ?>
<label for="name">Name:</label> <label for="name">Name:</label>
<input type="text" name="name" id="name" value="<?php echo $einzelmitglied['Name']; ?>" required> <input type="text" name="name" id="name" value="<?php echo $einzelmitglied['Name']; ?>" required>
@@ -157,6 +159,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
<!-- Formular für das Anlegen und Bearbeiten von Mitgliedern --> <!-- Formular für das Anlegen und Bearbeiten von Mitgliedern -->
<form method="post" action="<?php # echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>"> <form method="post" action="<?php # echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<input type="hidden" name="aktion" id="aktion" value="anlegen"> <input type="hidden" name="aktion" id="aktion" value="anlegen">
<?php echo app_csrf_field(); ?>
<label for="name">Name:</label> <label for="name">Name:</label>
<input type="text" name="name" id="name" required> <input type="text" name="name" id="name" required>
@@ -210,6 +213,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>"; echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>";
echo "<input type='hidden' name='aktion' value='bearbeiten'>"; echo "<input type='hidden' name='aktion' value='bearbeiten'>";
echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>"; echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>";
echo app_csrf_field();
echo "<button type='submit'>Bearbeiten</button>"; echo "<button type='submit'>Bearbeiten</button>";
echo "</form></li><li>"; echo "</form></li><li>";
@@ -218,12 +222,14 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>"; echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>";
echo "<input type='hidden' name='aktion' value='deaktivieren'>"; echo "<input type='hidden' name='aktion' value='deaktivieren'>";
echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>"; echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>";
echo app_csrf_field();
echo "<button type='submit'>Deaktivieren</button>"; echo "<button type='submit'>Deaktivieren</button>";
echo "</form>"; echo "</form>";
} else { } else {
echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>"; echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>";
echo "<input type='hidden' name='aktion' value='aktivieren'>"; echo "<input type='hidden' name='aktion' value='aktivieren'>";
echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>"; echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>";
echo app_csrf_field();
echo "<button type='submit'>Aktivieren</button>"; echo "<button type='submit'>Aktivieren</button>";
echo "</form>"; echo "</form>";
} }
+3 -1
View File
@@ -2,6 +2,7 @@
include "functions.php"; include "functions.php";
app_require_csrf();
include "header.php"; include "header.php";
include "headerline.php"; include "headerline.php";
include "nav.php"; include "nav.php";
@@ -84,6 +85,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["a
<form method="post" action="<?php echo $_SERVER["PHP_SELF"]; ?>"> <form method="post" action="<?php echo $_SERVER["PHP_SELF"]; ?>">
<?php echo app_csrf_field(); ?>
<label for="mitarbeiterID">Mitarbeiter auswählen:</label><br> <label for="mitarbeiterID">Mitarbeiter auswählen:</label><br>
<select name="mitarbeiterID" id="mitarbeiterID"> <select name="mitarbeiterID" id="mitarbeiterID">
<?php <?php
@@ -119,4 +121,4 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["a
</div> </div>
</div> </div>
<?php include "footer.php"; ?> <?php include "footer.php"; ?>