Abschnitt 11 · Lektion 4
Eingaben ausgeben
In den letzten drei Lektionen ist alles nur in eine Richtung gelaufen: Der Besucher schickt, du nimmst entgegen. Jetzt geht etwas zurück, und damit fängt der interessante Teil an.
Warum eine Ausgabe gefährlich ist
<?php
// Der Text geht hier unveraendert in die Seite. Im Feld steht
// schon etwas, druck einfach auf Absenden.
$istPost = $_SERVER["REQUEST_METHOD"] === "POST";
$text = $istPost ? trim($_POST["text"] ?? "") : "<b>fett</b> und <em>schräg</em>";
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Roh ausgegeben</title>
</head>
<body>
<h1>Roh ausgegeben</h1>
<form method="post" action="/">
<p><label>Text: <input type="text" name="text" size="40" value="<?= $text ?>"></label></p>
<button type="submit">Absenden</button>
</form>
<p>Du hast geschrieben: <?= $text ?></p>
<p>Probier auch <code><h1>riesig</h1></code>
oder <code><hr></code>.</p>
</body>
</html> Eine Seite ist Text, und der Browser liest ihn als Anweisung. Er sieht <b> und macht fett, er
sieht <h1> und macht eine Überschrift, er sieht <script> und führt aus, was darin steht.
Wenn du jetzt etwas in diese Seite schreibst, das ein Besucher getippt hat, dann schreibt in
Wirklichkeit er in dein Markup. Er darf also alles, was du darfst. Ein <b> ist dabei noch das
Harmloseste; das Beispiel zeigt es, weil man es sofort sieht.
Der Fachbegriff dafür ist Cross-Site-Scripting, kurz XSS, und der ausführliche Teil samt der Frage, was ein Angreifer damit wirklich anstellt, steht in Lektion 15.2. Für heute reicht die Beobachtung: Ein Wort, das du nicht selbst geschrieben hast, ist in deiner Seite kein Text, sondern Markup.
Eine Funktion, eine Regel
<?php
// Genau dieselbe Seite, nur mit htmlspecialchars an den beiden
// Stellen, an denen der Text hinausgeht.
$istPost = $_SERVER["REQUEST_METHOD"] === "POST";
$text = $istPost ? trim($_POST["text"] ?? "") : "<b>fett</b> und <em>schräg</em>";
$sicher = htmlspecialchars($text, ENT_QUOTES, "UTF-8");
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Entschärft</title>
</head>
<body>
<h1>Entschärft</h1>
<form method="post" action="/">
<p><label>Text: <input type="text" name="text" size="40" value="<?= $sicher ?>"></label></p>
<button type="submit">Absenden</button>
</form>
<p>Du hast geschrieben: <?= $sicher ?></p>
<p>Und so sieht es im Quelltext aus:</p>
<pre><?= htmlspecialchars($sicher, ENT_QUOTES, "UTF-8") ?></pre>
</body>
</html> htmlspecialchars() macht aus den Zeichen, die eine Bedeutung haben, harmlose Ersatzschreibweisen:
aus < wird <, aus > wird >, aus & wird &, aus " und ' werden
" und '.
Der Browser zeigt danach genau die Zeichen an, die der Besucher getippt hat. Aus einer Anweisung wird wieder Text, und das ist der ganze Trick.
Damit gilt ab hier für den Rest des Kurses eine Regel, und sie hat keine Ausnahme:
Alles, was nicht aus deinem eigenen Code kommt, geht durch htmlspecialchars(), bevor es in eine
Seite geschrieben wird. Formularfelder, Werte aus der Adresse, später Zeilen aus der Datenbank und
Text aus einer fremden Schnittstelle.
Umgekehrt gilt auch: Was du selbst getippt hast, brauchst du nicht zu entschärfen. Dein eigenes
<h1> soll ja eine Überschrift werden.
Die zwei Argumente dahinter
Geschrieben wird die Funktion im Kurs immer mit beiden Argumenten:
htmlspecialchars($wert, ENT_QUOTES, "UTF-8").
ENT_QUOTES heißt: beide Sorten Anführungszeichen mit entschärfen, die doppelten und die einfachen.
"UTF-8" sagt, in welcher Zeichenkodierung der Text vorliegt, damit Umlaute heil ankommen.
Seit PHP 8.1 sind beide Werte ohnehin die Vorgabe, du könntest sie also weglassen. Ich schreibe sie trotzdem hin, aus zwei Gründen: Auf einem älteren Hosting-Paket ist die Vorgabe eine andere und lässt die einfachen Anführungszeichen durch, und wer den Aufruf liest, sieht sofort, was gemeint ist, statt es aus einer Versionsnummer erschließen zu müssen.
Attribute vergisst man am ehesten
<?php
// Die Stelle, die am haeufigsten vergessen wird: ein Wert mitten
// in einem Attribut. Oben steht er mit ENT_COMPAT, unten mit
// ENT_QUOTES. Sieh dir an, welches Feld gelb wird.
function e(?string $wert): string
{
return htmlspecialchars($wert ?? "", ENT_QUOTES, "UTF-8");
}
$istPost = $_SERVER["REQUEST_METHOD"] === "POST";
$text = $istPost ? trim($_POST["text"] ?? "") : "Mia' style='background:gold";
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Attribute</title>
</head>
<body>
<h1>Der Wert mitten in einem Attribut</h1>
<form method="post" action="/">
<p><label>Text: <input type="text" name="text" size="40" value="<?= e($text) ?>"></label></p>
<button type="submit">Absenden</button>
</form>
<p>Mit ENT_COMPAT, also ohne die einfachen Anführungszeichen:</p>
<p><input type='text' size='40' value='<?= htmlspecialchars($text, ENT_COMPAT, "UTF-8") ?>'></p>
<p>Mit ENT_QUOTES:</p>
<p><input type='text' size='40' value='<?= e($text) ?>'></p>
</body>
</html> Zwischen zwei Tags denkt jeder daran. In einem Attribut nicht, und dabei ist es dieselbe Sache:
value="<?= $text ?>" ist genauso offen, nur endet der Ausbruch dort mit einem Anführungszeichen
statt mit einer spitzen Klammer.
Im Beispiel steht das obere Feld mit ENT_COMPAT da, was die einfachen Anführungszeichen
durchlässt. Der Wert schließt damit das Attribut und hängt ein eigenes style an, und das Feld wird
gelb. Das untere Feld nimmt ENT_QUOTES und bleibt weiß.
Eine Hilfsfunktion lohnt sich
Der Aufruf ist lang, und du brauchst ihn in jeder zweiten Zeile Markup. Deshalb schreibt man ihn einmal in eine kurze Funktion, meistens mit einem sehr knappen Namen:
Die Funktion aus dem dritten Beispiel heißt e, nimmt ein ?string entgegen und gibt einen string
zurück. Das Fragezeichen davor ist wichtiger, als es aussieht: Ein fehlendes Feld ist null, und
null direkt an htmlspecialchars() zu übergeben, ist seit PHP 8.1 verwarnt. Mit ?? "" in der
Funktion ist der Fall an genau einer Stelle erledigt statt an fünfzig.
Ab Lektion 11.5 taucht diese Funktion in jeder Aufgabe wieder auf, und in Abschnitt 16 zieht sie in eine eigene Datei um.
Zum Mitnehmen
Ein Besucher schreibt etwas in dein Feld, und du zeigst es wieder an. Klingt harmlos, ist es aber nicht: Was er schreibt, landet in deinem Markup, und Markup ist eine Anweisung an den Browser. Ab dieser Lektion gilt für den Rest des Kurses eine Regel ohne Ausnahme.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.