Zerlege XSS, CSRF und SQL-Injection, um genau zu verstehen, was htmlspecialchars, Tokens und Prepared Statements wirklich blockieren.
Diese Lektion in Kodokon öffnenDie drei großen Familien von Web-Schwachstellen - XSS, CSRF und Injections - haben alle dieselbe Wurzel: Vom Nutzer gelieferte Daten landen dort, wo sie als Code interpretiert werden (HTML, SQL oder eine legitime Anfrage). Beginnen wir mit XSS: Wenn du einen Wert aus $_GET ohne Escaping ausgibst, kann ein Angreifer ein <script>-Tag einschleusen und JavaScript in den Browsern deiner anderen Besucher ausführen - Sessions stehlen, Aktionen ohne deren Wissen ausführen. Mit ?comment=<script>alert(1)</script> führt die erste Ausgabe unten das Skript aus; die zweite entschärft es. Zwei Flags sind hier wichtig: ENT_QUOTES wandelt auch einfache Anführungszeichen um (sonst bleibt ein mit einfachen Anführungszeichen begrenztes Attribut angreifbar), und ENT_SUBSTITUTE ersetzt ungültige UTF-8-Sequenzen, statt eine leere Zeichenkette zurückzugeben. Seit PHP 8.1 sind beide Flags endlich der Standard, aber sie auszuschreiben dokumentiert deine Absicht und schützt Code, der auf älteren Versionen läuft.
<?php
declare(strict_types=1);
$comment = $_GET['comment'] ?? '';
echo '<div>' . $comment . '</div>';
function e(string $value): string
{
return htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
}
echo '<div>' . e($comment) . '</div>';CSRF nutzt eine einfache Tatsache aus: Der Browser hängt Session-Cookies automatisch an jede Anfrage an deine Domain, selbst an eine, die von einer fremden Website ausgelöst wurde. Ein feindliches Formular kann also /account/delete mit der Session des Opfers abschicken. Die klassische Verteidigung ist das Synchronizer-Token: ein zufälliges Geheimnis, das in der Session gespeichert und bei jeder verändernden Anfrage verlangt wird. Eine fremde Website kann es weder lesen noch erraten und deshalb auch nicht mitliefern.
<?php
session_start();
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$sent = $_POST['csrf'] ?? '';
$valid = is_string($sent)
&& hash_equals($_SESSION['csrf'], $sent);
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
if ($isPost && !$valid) {
http_response_code(403);
exit('Invalid CSRF token');
}
?>
<form method="post">
<input type="hidden" name="csrf"
value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
<button>Delete my account</button>
</form>SQL-Injection folgt demselben Muster: Einen Nutzerwert in eine Abfrage zu verketten heißt, den Nutzer SQL schreiben zu lassen - ' OR 1=1 -- macht aus einem Filter die ganze Tabelle. Die Lösung ist nicht Escaping, sondern das Prepared Statement: Der Abfragetext (mit seinen :email-Platzhaltern) und die Werte reisen getrennt zur Engine, die deshalb Daten niemals mit Code verwechseln kann. Ein Expertendetail: Bei MySQL emuliert PDO Prepared Statements standardmäßig und escapt auf der Client-Seite; setze PDO::ATTR_EMULATE_PREPARES auf false für wirklich native Prepared Statements.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION
);
$pdo->exec(
'CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL
)'
);
$email = $_GET['email'] ?? '';
$stmt = $pdo->prepare(
'SELECT id FROM users WHERE email = :email'
);
$stmt->execute(['email' => $email]);
var_dump($stmt->fetch(PDO::FETCH_ASSOC));