Sicher programmieren fürs Web

25.11.2003 von THOMAS WOELFER 
Bei der Erstellung von dynamischen Webseiten kann ein Entwickler mehr Fehler machen, als ihm oft bewusst ist. Welche vermeidbaren Fehler oder Unachtsamkeiten Angreifern Tür und Tor öffnen, lesen Sie in diesem Artikel.

Bei der Sicherung von Webseiten denkt man sofort an Defacing (Austausch der Homepage gegen eine andere) und vielleicht noch an Würmer oder unsichere Webserver. Diese Aspekte machen aber nur einen Teil der notwendigen Sicherheitsüberlegungen aus, um die man sich bei der Programmierung einer sicheren Webseite kümmern muss, wie die regelmäßig in unserem Secunia-Newsletter auftauchenden Berichte zeigen. Um eine Webseite effektiv abzusichern, kann man allerdings nicht einfach irgendein Programm oder eine Firewall einsetzen.

Fehler Nummer eins besteht darin, dass der Webmaster sich komplett auf eine Firewall verlässt, die vor dem Webserver steht. Das ist sträflich nachlässig, denn eine Firewall eignet sich zwar bestens dazu, Pakete auf Basis von Regeln zu blockieren, sie kümmert sich im Normalfall jedoch nicht um die Filterung von HTTP -Datenverkehr. Aber auch solcher Traffic kann durchaus seine gefährlichen Seiten haben.

Ähnlich wie eine Firewall schafft auch die Verwendung von "sicheren" Protokollen keine Abhilfe: SSL mag zwar Pakete verschlüsseln können, es kümmert sich aber nicht darum, dass die eingegebenen Daten validiert werden. Diese Daten werden jedoch an Programme weitergegeben und dort verarbeitet - mit dem "richtigen" Aufbau des Daten-Strings kann ein Angreifer durchaus die Datenbank auf dem Server löschen, beliebige Programme starten oder Ähnliches tun.

Das soll nicht bedeuten, dass Firewall oder SSL nicht sinnvoll sind - der Einsatz dieser Technologien ist schlicht und ergreifend nicht ausreichend. Um eine Site abzusichern, muss man sich zusätzlich um verschiedene grundlegende Dinge kümmern, ganz besonders bei der Programmierung der Site.

Sicherheits-Grundbausteine

Authentifizierung und Autorisierung stellen einen zentralen Grundbaustein der Webseiten-Sicherheit dar. Damit soll sichergestellt werden, dass nur berechtigte Benutzer, Dienste oder andere Computer auf bestimmte Bestandteile der Website zugreifen können.

Durch die Authentifizierung des Client weiß man, um wen es sich dabei handelt. Diese Information kann man auch später abrufen, um Geschehnisse nachzuvollziehen. Die Autorisierung stellt klar, was dieser eindeutig identifizierte Client tun darf und was nicht.

Ein weiteres wichtiges Ziel ist das Sicherstellen der Privatsphäre in einem gegebenen Kontext. Dabei geht es darum, dass vertrauliche Daten auch privat bleiben: Nicht autorisierte Personen sollen nicht in der Lage sein, Traffic mitzuhören oder auf gespeicherte Daten zuzugreifen, die einer anderen Person gehören oder nur in einem anderen Kontext zur Verfügung stehen können. Genauso wichtig ist in diesem Zusammenhang die Datenintegrität, also die Veränderung durch nicht autorisierte Personen.

Schließlich muss auch noch die Verfügbarkeit von Daten und Diensten Teil der Sicherheitsüberlegungen sein: Es gibt eine ganze Menge Personen, deren hauptsächliches Ziel die Ausführung von Denial-of-Service-Attacken ist. Können aber authentifizierte Personen deshalb nicht mehr auf die Dienste einer Webseite zugreifen, dann hat das den gleichen Effekt wie das Löschen der Inhalte durch einen Einbrecher.

Kategorien von Angriffen

Mögliche Angreifer können verschiedene Ziele durch einen Angriff verfolgen und je nach Ziel dabei verschiedene Methoden einsetzen. Durch Ziel und Methode lassen sich Angriffe kategorisieren. Die folgenden Kategorien sind die wichtigsten:

Denial of Service: Hier geht es darum, die Verfügbarkeit eines Dienstes zu reduzieren oder ihn komplett unzugänglich zu machen. Das kann der Angreifer zum Beispiel erreichen, indem er eine Webanwendung mit Requests bombardiert, bis die Anwendung nicht mehr oder nicht in einem adäquaten Zeitrahmen antwortet.

Rechteübernahme: Ein Angreifer kann versuchen, seine Privilegien - die er als ganz normaler öffentlicher User einer Webanwendung hat - so zu steigern, dass er mehr Kontrolle über die Anwendung selbst oder über weiter gehende Prozesse auf dem Server der Anwendung erhält.

Weitergabe von Informationen: Der Angreifer will möglichst viele interne Informationen über eine Webseite erhalten. Diese Informationen kann er zum Beispiel durch fälschlicherweise öffentlich zugängliche Fehlermeldungen oder durch das Abhören von unverschlüsseltem IP-Traffic erlangen.

Spoofing: Hier versucht der Angreifer, Zugang zu Daten oder Diensten zu erhalten, indem er sich als ein anderer User ausgibt. Das kann beispielsweise durch eine gefälschte IP-Adresse geschehen oder über gestohlene Account-Daten.

Buffer Overflows

Um einem Angriff gegen eine Webanwendung vorzubeugen, sind verschiedene Maßnahmen notwendig. Die Wichtigste ist die Validierung von Benutzereingaben. Viel zu oft gehen Entwickler von falschen Voraussetzungen bei den eingehenden Daten aus, und dieses Manko nutzen Angreifer aus.

Daher sollte eine Anwendung eingegebenen Daten niemals blind vertrauen, andernfalls ist sie durch verschiedene Arten von Angriffen verwundbar. Dabei handelt es sich im Wesentlichen um Buffer Overflow, Cross-Site-Scripting, SQL Injection und Canonicalization.

Über einen Buffer Overflow kann der Angreifer einen Denial-of-Service hervorrufen oder im schlimmsten Fall die Ausführung beliebigen Codes erreichen. Ein Buffer Overflow tritt immer dann auf, wenn Eingangsdaten in einen Buffer kopiert werden, der für die übermittelte Datenmenge nicht groß genug ist.

void foo( const char* psz)
{
char sz[16];
strcpy( sz, psz);
}

Ist der an die Funktion übergebene String größer als der reservierte Zeichenpuffer, tritt ein Buffer Overflow auf, denn die Funktion strcpy kopiert bis zur abschließenden Null alle Zeichen aus dem Quell-String in den Puffer und überschreibt dabei auch die Rücksprungadresse der Funktion. Statt die Funktion zu verlassen, wird Code ausgeführt, der sich ebenfalls in den vom Angreifer übergebenen Daten befindet. Dieser Code läuft dann mit den Privilegien des Accounts, unter dem der ursprüngliche Code betrieben wurde.

Im Rahmen von .NET stellen Buffer Overflows kein Problem mehr dar, denn bei reinem .NET-Code werden die Grenzen von Arrays bei Zugriffen grundsätzlich getestet - hier können Buffer Overflows nicht auftreten. Ruft der .NET-Code hingegen unmanaged Code auf, wenn Sie beispielsweise auf COM-Objekte zugreifen, ist die Gefahr eines Buffer Overflow weiterhin gegeben.

Wenn Sie Ihre Webanwendungen mit C++ programmieren, dann sollten Sie auf jeden Fall den Compiler-Switch "/GS" benutzen: Dieser fügt Sicherheitsüberprüfungen in Ihren Code ein, die Sie gegen eine ganze Reihe von potenziellen Buffer Overflows schützen. Leider aber nicht gegen alle: Sie müssen also Aufrufe von unmanaged Code auf jeden Fall gegen Buffer Overflows testen, um die Sicherheit zu gewährleisten.

Cross-Site-Scripting

Cross-Site-Scripting dient nicht direkt dazu, eine Website anzugreifen. Stattdessen wird der Rechner eines Users angegriffen, der diese Webseite gerade besucht. Die Webseite dient nur als Transportmittel für den Angriff. Dazu muss der Angreifer den User erst dazu bewegen, auf einen Link zu klicken, der auf Ihre Webseite zeigt. Das kann zum Beispiel durch einen HTML-Link in einer E-Mail geschehen.

Der Link zeigt dabei auf eine normale Seite Ihrer Webseite, enthält aber zusätzlich ein Script, das der Angreifer hinten an den Link angehängt hat. Dies sieht dann beispielsweise so aus:

http://www.dieanwendung.de/login.asp?param=<script>alert("hallo")</script>

Wenn die Webseite die als Parameter übergebenen Werte nun an den aufrufenden Browser zurückgibt, etwa weil sie eine Meldung in der Art von "Sie haben xyz eingegeben" erstellt, dann führt dies dazu, dass das vom Angreifer übergebene Script im Browser des Benutzers ausgeführt wird - und zwar mit den Rechten der besuchten Website. Mit einem passenden Script könnte der Angreifer nun zum Beispiel Cookies des Users auslesen und an sich selbst zustellen. Enthalten die Cookies Login-Daten des Users, hat der Angreifer dadurch Zugriff auf fremde Account-Daten erhalten. Somit lässt sich Cross-Site-Scripting durchaus für Angriffe auf Websites nutzen.

Angriffen per Cross-Site-Scripting können Sie auch mit Input-Validierung entgegenwirken. Sie müssen sicherstellen, dass sämtliche Eingaben für Queries, Form-fields und Cookies gültige und zulässige Daten beinhalten. Sie sollten grundsätzlich davon ausgehen, dass die eingegebenen Daten ungültig und bösartig sein können. Ferner sollten Sie alle Daten, die ein User eingeben kann und die im Browser angezeigt werden können, mit HTMLEncode() und URLEncode() bearbeiten. Das stellt sicher, dass alle möglicherweise eingegebenen Scripts in normales und völlig ungefährliches HTML umgewandelt werden, bevor Sie im Browser des Besuchers erscheinen.

SQL-Injection

Unter SQL-Injection versteht man einen Angriff, bei dem der Angreifer versucht, beliebige Kommandos innerhalb Ihres Datenbank-Servers auszuführen. Das funktioniert einfacher, als man es sich vorstellen mag. Hier liegt der Fehler ebenfalls in nicht richtig validierten Benutzereingaben begründet. Kann der Angreifer erst einmal beliebige Kommandos im Kontext des Datenbank-Servers ausführen, dann ist er nur noch einen kleinen Schritt davon entfernt, beliebige Betriebssystemkommandos auszuführen.

Das Ausführen eines beliebigen SQL-Befehls mit Hilfe von SQL-Injection erfolgt auf Basis von passend formulierten Strings, die ohne Validierung an die Datenbank weitergegeben werden. Alternativ kann das gleiche Problem auch daraus resultieren, dass das Script vom Benutzer eingegebene Strings dafür verwendet, SQL-Statements dynamisch zusammenzusetzen.

Angenommen Sie haben die folgende SQL-Abfrage kodiert:

string query = "select * from user_table where username = '" + username + "'";

Die Variable username enthält ihrerseits einen Text, den der User eingeben kann - zum Beispiel über einen Login-Dialog. Der Angreifer gibt nun nicht seinen Namen ein, sondern den folgenden String:

'; delete from user_table

Dadurch enthält der Query-String de facto zwei SQL-Kommandos, die vom Datenbank-Server ausgeführt werden. Der Query-String hat in seiner expandierten Form dann nämlich den folgenden Inhalt:

Select * from user_table where username='; delete from user_table

Mit anderen Worten: Ihre Tabelle user_table ist danach leer! Zumindest wenn die Query unter einem Account ausgeführt wird, der das Recht zum Löschen von Daten hat. (Woran man auch leicht sehen kann, dass es enorm wichtig ist, die Rechtevergabe und die verwendeten Accounts bei allen Datenbankzugriffen zu überprüfen.)

Auch bei SQL-Injection gilt: Testen Sie Benutzereingaben. Bei Daten, die Sie an eine Datenbank weiterreichen, sollten Sie alle Zeichen ausfiltern, die vom SQL-Server als Sonderzeichen interpretiert werden - wie zum Beispiel das Semikolon oder Hochkommata.

Im Zusammenhang mit dem Beispiel ist noch anzumerken, dass der Angreifer nur dann Erfolg haben kann, wenn er die Namen Ihrer Tabellen kennt. Diesen Namen kann er aber vorher auf andere Art herausgefunden haben: Zum Beispiel dadurch, dass er an anderer Stelle ungültige Daten eingegeben hat, die eine Fehlermeldung mit internen Informationen verursacht haben, etwa ein Statement wie "Diese Daten sind für die tabelle user_table nicht zulässig".

Input-Validierung

Das Problem bei der Validierung von Benutzereingaben ist die Tatsache, dass man nicht pauschal zwischen "gültigen" und "ungültigen" Eingaben unterscheiden kann. So können Sie beispielsweise nicht einfach alle Hochkommata, Semikolons und andere Zeichen aus allen Texten herausfiltern: Das macht es dem Benutzer nämlich nicht unbedingt einfacher, sinnvolle Texte einzugeben.

Sie müssen stattdessen im Einzelfall planen und überprüfen, welche Daten sinnvoll sind und welche nicht. Trotzdem sollten Sie die Validierung unbedingt an einer zentralen Stelle durchführen: Das erleichtert die Wartung des Codes ungemein - außerdem kann man die Validierungsroutinen dadurch einfacher wiederverwenden.

Beginnen Sie bei der Validierung damit, dass Sie zulässige Daten festlegen. Dabei sollten Sie auf jeden Fall auch die Datentypen berücksichtigen: Datentypen sind deutlich leichter zu testen, als das bei einfachen Strings der Fall ist - vor allem hinsichtlich der zulässigen Wertebereiche. Die Zulässigkeit von Daten definieren Sie dann in Form von Datentyp, Wertebereich, Format und Länge. Alle eingehenden Daten, die den gesetzten Anforderungen nicht entsprechen, weisen Sie als unzulässig zurück, verarbeiten sie also nicht weiter. Das gilt ganz besonders für Strings, denn hier gibt es oft Anforderungen an die Länge und die Formatierung, etwa bei Postleitzahlen oder Geldbeträgen, die als Text übergeben werden.

Konvertieren Sie Daten außerdem immer in ein sicheres, bekanntes Format. Dazu zählt zum Beispiel das Entfernen von Nullwerten aus Strings und das Umsetzen (Escapen) von Sonderzeichen. Mit ASP sollten Sie außerdem die Funktionen HtmlEncode() und UrlEncode() verwenden. Damit konvertieren Sie Daten in normale String-Literals, so dass zum Beispiel eingebettete Scripts nur als Text und nicht mehr als Script behandelt werden.

Zur Validierung von Textfeldern können Sie beispielsweise den RegularExpressionValidator aus der .NET-Klassenbibliothek benutzen. Dieser überprüft, ob das Textfeld einem gegebenen regulären Ausdruck entspricht. Typische reguläre Ausdrücke zum Test von Eingabefeldern stehen direkt innerhalb des RegularExpressionValidator zur Verfügung.

Im Rahmen von .NET, ASP und ASPX gibt es noch eine Vielzahl von weiteren Punkten, auf die Sie beim Programmieren achten sollten, und eine Vielzahl von Möglichkeiten. Bei MSDN finden Sie ein ganzes Online-Buch, das sich mit der Sicherheit von Webanwendungen auseinander setzt. Dabei werden auch Aspekte wie Netzwerksicherheit und die Sicherheit von Hosts berücksichtigt. "Improving Web Application Security: Treats and Countermeasure" finden Sie unter dieser URL.

Fazit

Die Programmierung von sicheren Webanwendungen ist nicht einfach: Zusätzlich zu alltäglichen Problemen wie Buffer Overflows, die man auch bei der ganz normalen Programmierung berücksichtigen muss, kommen spezielle Netzwerkgegebenheiten, Eingabevalidierung und viele andere Probleme zum Bündel der zu beachtenden Schwierigkeiten hinzu. Daher sollten Sie beim Erstellen von Webanwendungen auch nie statische Regeln anwenden. Stattdessen empfiehlt es sich, auf jeden Fall regelmäßig die verwendeten Designs zu benutzen, die Validierung zu überprüfen und den vorliegenden Code gegen die denkbaren Attacken zu testen. (mha)