Zum Inhalt

Beispielarbeiten · Code & Fehler

PHP: Kontaktformular und Absicherung gegen SQL-Injection und XSS

Ein Kontaktformular mit Speicherung und eine verwundbare Suchseite, die ich gegen SQL-Injection und XSS abgesichert habe. Die Sicherheitsfälle waren vorher rot, danach grün.

Musterarbeit (Demo), kein Kundenauftrag. Gebaut als Probeaufgabe mit erfundenen Beispieldaten; die Prüfzahlen unten stammen aus dem automatischen Prüflauf und sind nicht geschönt.

Was gebaut wurde

  • Kontaktformular mit Prüfung der Eingaben, Schutz vor Formularmissbrauch und Speicherung
  • Suchseite vorher mit SQL-Injection und ungeschützter Ausgabe, nachher mit Prepared Statement und maskierter Ausgabe
  • Gegenprobe mit absichtlich kaputten Fassungen: die Sicherheitsfälle schlagen an
  • Alle Dateien syntaktisch geprüft, Diff zum Nachlesen

kontakt.php herunterladen liste.php herunterladen lib.php herunterladen bugfix.diff herunterladen

Ohne Laravel, Symfony und Composer. Getestet mit dem eingebauten PHP-Server und SQLite, nicht auf Apache, nginx oder MySQL. Die Sicherheitsfälle decken SQL-Injection, XSS und CSRF ab und sind kein vollständiger Penetrationstest.

Wie geprüft wurde

Automatischer Prüflauf vom 01.11.2026, aus dem Protokoll (Testdaten erfunden, Zahlen gemessen):

  • php -l 5/5 fehlerfrei
  • Kontakt 25/25 gruen
  • Liste vorher verwundbar: 6/6 Sicherheitsfaelle rot
  • nachher 11/11 gruen
  • Gegenprobe 3/3 kaputte Fassungen erkannt

Grenzen

Was diese Probe nicht abdeckt, steht hier, damit niemand mehr erwartet:

  • ohne Laravel / Symfony / Composer (Frameworks und Paketverwaltung nicht freigegeben): Kundenprojekte, die sie verlangen, brauchen erst eine Freigabe des Owners
  • PHP 8.3 (NTS, eingebauter Webserver php -S) - kein Apache/nginx/FPM, MySQL nur ueber PDO-Code, nicht gelaufen (Test mit SQLite)
  • keine Arbeit auf dem Server des Kunden ohne Zugang; Ergebnis ist Quellcode + Testnachweis
  • Sicherheitsprueffall deckt SQL-Injection, XSS und CSRF ab - kein vollstaendiges Penetrationstest-Ergebnis

Die Absicherung als Diff

--- liste.php (vorher)
+++ liste.php (nachher)
@@ -3,21 +3,28 @@
 
 require __DIR__ . '/lib.php';
 
-// Oeffentliche Nachrichten durchsuchen (Kundenseite, fehlerhaft).
+// Oeffentliche Nachrichten durchsuchen.
+// Behoben: SQL-Injection (Prepared Statement, LIKE-Zeichen maskiert) und XSS (htmlspecialchars an jeder Ausgabe).
 $q = $_GET['q'] ?? '';
+if (!is_string($q)) {
+    $q = '';
+}
 $pdo = db();
-$sql = "SELECT id, name, nachricht FROM nachrichten WHERE oeffentlich = 1 AND nachricht LIKE '%" . $q . "%' ORDER BY id";
-$zeilen = $pdo->query($sql)->fetchAll();
+$stmt = $pdo->prepare(
+    "SELECT id, name, nachricht FROM nachrichten WHERE oeffentlich = 1 AND nachricht LIKE :q ESCAPE '\\' ORDER BY id"
+);
+$stmt->execute([':q' => '%' . addcslashes($q, '%_\\') . '%']);
+$zeilen = $stmt->fetchAll();
 
 echo kopf('Nachrichten');
 echo '<h1>Nachrichten</h1>';
-echo '<form method="get" action="/liste.php"><input name="q" value="' . $q . '"> <button>Suchen</button></form>';
+echo '<form method="get" action="/liste.php"><input name="q" value="' . h($q) . '"> <button>Suchen</button></form>';
 if ($q !== '') {
-    echo '<p>Suche nach: ' . $q . '</p>';
+    echo '<p>Suche nach: ' . h($q) . '</p>';
 }
 echo '<ul>';
 foreach ($zeilen as $z) {
-    echo '<li><strong>' . $z['name'] . '</strong>: ' . $z['nachricht'] . '</li>';
+    echo '<li><strong>' . h($z['name']) . '</strong>: ' . h($z['nachricht']) . '</li>';
 }
 echo '</ul>';
 echo fuss();

So etwas für deinen Betrieb?

Beschreib kurz, was du brauchst. Du bekommst einen Festpreis, bevor ich anfange; ohne dein Ja passiert nichts.

Zur Werkstatt