Kodokon kodokon.com

Web-Sicherheit: XSS, CSRF und Injections

Zerlege XSS, CSRF und SQL-Injection, um genau zu verstehen, was htmlspecialchars, Tokens und Prepared Statements wirklich blockieren.

12 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Die 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
<?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>';
Die erste Ausgabe ist verwundbar, die zweite ist korrekt escaped.

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.

HTML
<?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>
Token einmal pro Session erzeugt, vor jeder verändernden Aktion geprüft.

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
<?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));
Der Wert landet nie im SQL-Text: Injection ist unmöglich.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum übergibst du htmlspecialchars das Flag ENT_QUOTES?
    • Um auch einfache Anführungszeichen umzuwandeln, ohne die ein mit einfachen Anführungszeichen begrenztes Attribut angreifbar bleibt
    • Um die Zeichenkette zwingend nach UTF-8 zu konvertieren
    • Um script-Tags aus dem Inhalt zu entfernen
    • Um das Escaping langer Zeichenketten zu beschleunigen
  2. Warum vergleichst du das CSRF-Token mit hash_equals statt mit ===?
    • hash_equals vergleicht in konstanter Zeit, was verhindert, dass man das Token Byte für Byte durch Messen der Antwortzeiten errät
    • hash_equals berechnet den Hash des Tokens vor dem Vergleich automatisch neu
    • === schlägt fehl, sobald die beiden Zeichenketten unterschiedliche Kodierungen haben
  3. Was macht ein Prepared Statement immun gegen SQL-Injection?
    • Es escapt automatisch die Anführungszeichen, die in den Werten stecken
    • Abfrage und Werte werden getrennt an die Engine geschickt, die die Werte nie erneut als SQL interpretiert
    • Es prüft, ob jeder Wert zum Typ der Zielspalte passt
    • Es beschränkt die Ausführung auf eine einzige SQL-Anweisung