XSS in PHP-Anwendungen: htmlspecialchars ist nicht alles

Keine Zeit? Hier die Kurzversion in 30 Sekunden.
- Beim reflektierten XSS kommt der Schadcode aus der URL, beim gespeicherten liegt er in der Datenbank.
htmlspecialcharsimmer mitENT_QUOTESund Zeichensatz aufrufen.- Je nach Kontext (HTML, Attribut, JavaScript, URL) braucht man ein anderes Escaping.
strip_tagsist keine Lösung, weder für Attribute noch für Skripte.- Session-Cookies mit dem HttpOnly-Flag schützen vor dem einfachsten Diebstahl.
Cross-Site-Scripting steht bei fast jeder Liste der häufigsten Webfehler weit oben, und ich sehe es immer wieder im Code. Meist steht da ein htmlspecialchars und alle denken, damit sei alles erledigt. Ist es leider nicht.
Zwei Arten
Beim reflektierten XSS wird ein Wert aus der Anfrage direkt wieder ausgegeben:
<?php
// suche.php?q=<script>alert(1)</script>
echo 'Ihre Suche: ' . $_GET['q'];Der Angreifer schickt dem Opfer einen präparierten Link. Beim gespeicherten XSS landet der Schadcode zuerst in der Datenbank, etwa in einem Kommentar, und trifft später jeden, der die Seite aufruft. Das ist meist schlimmer.
Warum ist das so ein hartnäckiges Problem? Weil es nicht auffällt, solange man nur normale Texte eingibt. Alles läuft, die Seite sieht gut aus, und erst ein böswilliger Besucher zeigt, was fehlt. Ich teste daher bei eigenen Formularen inzwischen immer mit einem <script>-Tag im Namensfeld, einfach um zu sehen, was passiert.
Richtig escapen
Die Grundregel: Alles, was von außen kommt, beim Ausgeben escapen, nicht beim Speichern.
<?php
echo htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8');Zwei Dinge werden oft vergessen. Ohne ENT_QUOTES bleiben einfache Anführungszeichen unangetastet, und ohne Zeichensatz nimmt PHP ISO-8859-1. Bei einer UTF-8-Seite kann das mit manchen Eingaben danebengehen. Also immer beides angeben.
Der Kontext zählt
HTML-Escaping passt nur für normalen Text und Attributwerte in Anführungszeichen. Schon bei Attributen ist ein Fehler schnell gemacht:
<input type="text" value="<?php echo htmlspecialchars($wert, ENT_QUOTES, 'UTF-8'); ?>">Die Anführungszeichen um den Wert sind Pflicht, sonst reicht ein Leerzeichen plus onmouseover=..., und der Angreifer hat gewonnen.
In JavaScript hilft htmlspecialchars nicht. Hier ist json_encode der bessere Weg (ab PHP 5.2, die Eingabe muss UTF-8 sein):
<script type="text/javascript">
var name = <?php echo json_encode($name); ?>;
</script>Bei URLs braucht man urlencode für die Parameter, und das Ganze dann zusätzlich für das HTML-Attribut:
<a href="suche.php?q=<?php echo htmlspecialchars(urlencode($q), ENT_QUOTES, 'UTF-8'); ?>">Suche</a>Und wenn Nutzer komplette Links angeben dürfen: Prüft, dass sie mit http:// oder https:// anfangen. javascript: im href ist sonst ein offenes Scheunentor.
Das Gleiche gilt für Ausgaben, die man selbst für harmlos hält: Werte aus der Datenbank, Cookies, Header wie der Referer oder sogar die eigene $_SERVER['PHP_SELF']. Wer sich angewöhnt, wirklich alles beim Ausgeben zu escapen, fährt am besten. Eine kleine Hilfsfunktion, die htmlspecialchars mit den richtigen Parametern kapselt, spart Tipparbeit und verhindert, dass jemand ENT_QUOTES vergisst.
strip_tags reicht nicht
Immer wieder lese ich den Tipp, einfach strip_tags zu benutzen. Das entfernt nur Tags. Steht die Eingabe aber in einem Attribut, braucht der Angreifer gar keinen Tag: " onmouseover="alert(1) reicht. Wer bestimmte Tags erlaubt, behält außerdem deren Attribute, und damit etwa onclick. Als Ersatz für sauberes Escaping taugt es nicht.
HttpOnly-Cookies
Das erste Ziel eines XSS ist oft das Session-Cookie. Seit PHP 5.2 kann man es per HttpOnly-Flag vor JavaScript verstecken:
<?php
ini_set('session.cookie_httponly', 1);
// oder für eigene Cookies, siebter Parameter
setcookie('name', $wert, 0, '/', '', false, true);Das behebt die Lücke nicht, macht den einfachsten Angriff aber schwerer. Betrachtet es als zweite Verteidigungslinie.
Wie macht ihr das? Escapt ihr von Hand oder habt ihr eine Hilfsfunktion, die ihr überall benutzt? Schreibt es gern in die Kommentare.
Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (PHP-Logo: Colin Viebrock), jeweils gemeinfrei; die Marken gehören ihren Inhabern.
Christian Ackermann
Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.