mitmario.dev

Eingaben ausgeben

PHP Sandbox 3 Min Lesezeit 3 BeispieleLektion 4 von 7

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

Was ein Besucher hineinschreiben kann
<?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>&lt;h1&gt;riesig&lt;/h1&gt;</code>
    oder <code>&lt;hr&gt;</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

Dieselbe Seite, entschärft
<?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 &lt;, aus > wird &gt;, aus & wird &amp;, aus " und ' werden &quot; und &#039;.

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

Der Wert mitten in einem Attribut
<?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, kostenlos

Zu 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.