Tipps zur Verhinderung von SQL-Injection für Webprogrammierer
Heutzutage sieht man selten eine wirklich statische Website oder App. Anstelle der früher so üblichen hartcodierten Daten und Tabellenlayouts wird die moderne Webentwicklungslandschaft von The Feed dominiert: einer scheinbar endlosen Schriftrolle dynamischer Inhalte.
Feeds wie diese werden durch das Abrufen von Daten aus einer Datenbank erstellt. Dadurch können Sie Dinge wie Tweets oder Statusaktualisierungen filtern, ohne die Seite wechseln zu müssen, und bedeutet, dass Ihr Service viel stärker automatisiert werden kann als in den vergangenen Jahrzehnten. Die gängigsten Datenbanktypen basieren auf der sehr beliebten Structured Query Language (SQL), bei der es sich einfach um die Programmiersprache handelt, die die Datenbank zum Speichern und Abrufen von Daten verwendet. Jede Tabelle in der Datenbank kann man sich wie eine Tabellenkalkulation vorstellen, deren jede Zelle die Datenpunkte enthält.
SQL Injection ist eine spezielle Angriffsart, die auf diese Art von Online-Apps abzielt. Während die Datenbank selbst möglicherweise vor Hackerangriffen geschützt ist, liegt die Schwachstelle bei diesen Angriffen in der Anwendung und deren Zugriffsebene auf die Datenbank. Der Angriff bringt die Anwendung dazu, zusätzliche SQL-Befehle an die Datenbank zu übergeben, meist mit der Absicht, entweder erhöhte Zugriffsrechte zu erhalten oder die Datenbank mehr Informationen auszugeben, als sie sollte.
Wir werden die drei wichtigsten Möglichkeiten behandeln, wie ein Programmierer seinen Code gegen diese Art von Angriff schützen kann. Wir stellen außerdem Beispiele von Java-, PHP-, Python- und Perl-Code zur Verfügung, um Ihnen zu helfen, zu verstehen, wie diese Lösungen bereitgestellt werden. Es gibt auch Möglichkeiten, wie ein Datenbank- oder Serveradministrator seinen Server gemeinsam mit dem Programmierer härten kann. Diese sind:
- Verwendung von Platzhaltern in vorbereiteten Abfragen
- Bereinigen der Benutzereingaben
- Die Verwendung gespeicherter Prozeduren
Und für die Server-/Datenbankadministratoren:
- Zugriffsrechte mit geringsten Privilegien
Platzhalter
Wir befassen uns zunächst mit der Verwendung von Platzhaltern in vorbereiteten SQL-Befehlen, die in den Quellcode des Programms eingebettet sind. Der Platzhalter ist einfach „?“ und wird in die SQL-Abfrage anstelle eines noch bereitzustellenden Werts eingefügt. Vom Programm wird dann erwartet, dass es dieses „?“ durch einen Wert ersetzt, der entweder vom Benutzer oder von einem anderen Abschnitt des Programms bereitgestellt wird.
Nehmen Sie zur Veranschaulichung des Konzepts das folgende SQL-Abfragebeispiel:
|_+_|Die Ausgabe dieser Abfrage ist jede Datenspalte in der Tabelle des Clients, die sich auf den Client mit der Client-ID 1078 bezieht. Nehmen wir nun an, dass das Programm den Benutzer auffordert, seine eigene Client-ID anzugeben. Die unsichere Version derselben Abfrage könnte etwa so aussehen:
|_+_|In diesem Beispiel wird vom Benutzer, der auf das Programm zugreift, erwartet, dass er einfach seine ID in das richtige Feld eingibt, um seine Kontodaten zu erhalten. Wenn der Benutzer jedoch die Syntax der Anweisung verletzt (z. B. indem er „0 ODER 1=1“ anstelle einer gültigen Client-ID eingibt), wird die Datenbankabfrage nun wie folgt gesendet:
|_+_|Da 1 immer gleich 1 ist, spuckt die obige Abfrage den Inhalt jeder Zeile in der Tabelle des Clients aus. Dazu gehören alle Kunden-IDs sowie alle in dieser Tabelle gespeicherten Informationen zum Kundenstamm eines Unternehmens. Sie möchten nicht, dass jeder Zugriff darauf hat. Die erste Möglichkeit, diese Art von Leck zu vermeiden, ist die Verwendung vorbereiteter Anweisungen mit Platzhaltern für die dynamisch generierten Werte.
Vorbereitete Stellungnahmen
SQL beinhaltet bereits eine Option zur Verwendung von Platzhaltern in einer dynamisch generierten SQL-Prepared-Anweisung. Der Platzhalter kann dann unmittelbar vor der Ausführung der Abfrage durch den richtigen Wert ersetzt werden. Eine vorbereitete Anweisung ist einfach eine SQL-Abfrage, die frühzeitig eingerichtet wird, um mehrfach verwendet zu werden, ohne dass die gesamte Abfrage erneut an die Datenbank gesendet werden muss. Stattdessen wird die vorbereitete Anweisung einmal gesendet und dann werden die unterschiedlichen Werte nacheinander gesendet.
In Ihrem Programm bereiten Sie Ihre SQL-Abfrage oder -Anweisung mit einem Platzhalter anstelle des Datenfelds vor, das der Benutzer bereitstellen soll. Bei unserer obigen Beispielabfrage fragt das Programm den Benutzer beispielsweise nach seiner Kunden-ID, damit er seine Kontodetails einsehen kann. Anstatt eine Variable direkt in der Abfrage zu platzieren, möchten Sie dort stattdessen ein „?“ platzieren. Es wird ungefähr so aussehen:
|_+_|Das Programm hat dann die Aufgabe, einen Wert bereitzustellen, der den Platzhalter ersetzt. Dies kann eine zuvor definierte Variable, eine Benutzereingabe oder sogar eine Werteliste aus einem anderen Programm sein. Sobald der Wert ersetzt wurde, kann die Abfrage ausgeführt und die Ergebnisse verarbeitet werden.
Platzhalter bestechen durch ihre Einfachheit. Die Datenbank selbst filtert viele unerwartete oder potenziell gefährliche Daten des Benutzers heraus, basierend auf dem, worauf der Platzhalter verweist. Wenn Ihr Benutzernamensfeld beispielsweise nur alphanumerische Eingaben akzeptiert, werden alle zusätzlichen Daten einfach aus der Abfrage gelöscht, ohne dass sie überhaupt verarbeitet werden.
Wenn der Wert von einem Benutzer bereitgestellt werden soll, muss das Programm natürlich sicherstellen, dass die Eingabe des Benutzers keinen zusätzlichen SQL-Code enthält. Das Programm muss zuerst die Eingabe bereinigen, bevor es die Abfrage ausführen kann.
Bereinigen von Benutzereingaben
Die Bereinigung der Benutzereingaben bedeutet einfach, sicherzustellen, dass der Benutzer nur das eingibt, was Sie von ihm erwarten. Alles, was der Benutzer eingibt und nicht mit den Erwartungen des Programms übereinstimmt, wird automatisch verworfen und ein Fehler generiert, bevor überhaupt etwas in die Datenbank gelangt. Diese Methode der reinen Validierung ist besser, als dass der Benutzer alles eingeben kann, was er möchte, und dann versucht wird, einen regulären Ausdruck zu erstellen, der ausgeworfen wird oder einen Fehler verursacht, wenn zusätzlicher SQL-Code erkannt wird.
Nehmen wir in unserem obigen Beispiel zunächst an, dass die clientID eine Zeichenfolge ist. Das Programm kann die Benutzereingabe auf eine Whitelist zulässiger Zeichen beschränken, wobei die maximale Länge die Länge der längsten Client-ID nicht überschreitet und keine Leerzeichen zulässig sind. Wenn die Client-ID eine Zahl wäre, beschränken Sie die Benutzereingabe auf Ganzzahlen einer bestimmten Länge: nicht mehr und nicht weniger.
Für Felder, die eine Mischung verschiedener Zeichentypen erfordern, wie etwa eine E-Mail-Adresse, benötigt das Programm möglicherweise dennoch einen regulären Ausdruck, um die Benutzereingaben zu bereinigen. Erstens, um sicherzustellen, dass das Programm eine tatsächliche E-Mail-Adresse erhält, und zweitens, um sicherzustellen, dass nur eine E-Mail-Adresse angegeben wird.
Ein regulärer Ausdruck ist ein definiertes Suchmuster, das auf einer festgelegten Zeichenkette basiert. Ein Beispiel für einen regulären Ausdruck speziell zur Validierung von E-Mail-Adressen sieht so aus:
|_+_|Es ist weitaus besser, Benutzereingaben auf die Whitelist zu setzen, indem man nur erwartete Informationen akzeptiert und alles andere ablehnt, als Benutzereingaben auf die Blacklist zu setzen, indem man die bereitgestellten Informationen nach verdächtig aussehenden Informationen durchsucht. Bei einer Blacklist besteht immer die Möglichkeit, dass der reguläre Ausdruck etwas wirklich Unerwartetes übersieht.
Ein kurzes Beispiel: Vor einigen Jahren fand ein Hacker heraus, dass er es konnte Geben Sie einen negativen Wert ein indem er über die Online-Banking-App seiner Bank Geld von seinem Bankkonto auf das Bankkonto eines Freundes überweist. Das Ergebnis war, dass er effektiv Geld vom Konto seines Freundes stahl. Da er ein ethischer Mensch ist, zeigte er dies nicht nur zum Spaß seinem Freund, sondern teilte dies auch der Bank mit.
Die Programmierer der Online-App dieser Bank hatten nicht vorhergesehen, dass ein Benutzer einen negativen Wert in das Eingabefeld für eine Geldüberweisung von einem Konto auf ein anderes eingeben würde, daher stand es nicht auf ihrer schwarzen Liste. Dies ist die einfachste Form eines SQL-Injection-Angriffs, die ich im Internet finden konnte und erfordert keine SQL- oder Programmierkenntnisse. Es brauchte nur ein wenig Neugier und ein gültiges Benutzerkonto mit ausreichenden Rechten, um eine Transaktion durchzuführen.
Gespeicherte Prozeduren
Gespeicherte Prozeduren können sehr sicher sein, wenn die zugrunde liegenden SQL-Transaktionen statisch sind. Zum Beispiel, wenn die Daten, auf die zugegriffen wird, nicht von Eingaben des Benutzers, von einem anderen Programm oder von einer zuvor im Programm festgelegten Variablen abhängig sind, die von einer anderen Quelle als dem lokalen Server abgerufen wird. Wenn die angezeigten Daten auf einer Umgebungsvariablen basieren, wie dem aktuellen Datum, dem geografischen Standort des Benutzers oder einem Benutzernamen, gelten die Daten als statisch.
Wenn die SQL-Transaktion jedoch auf dynamisch generierten Daten basiert, sollte sie als verdächtig behandelt werden, bis sie validiert werden kann. Alles, was der Benutzer während der Laufzeit des Programms bereitstellt, gilt automatisch als gefährlich. Ebenso sollten alle von einem anderen Programm generierten Informationen als verdächtig betrachtet werden, bis sie bereinigt oder verworfen werden.
Beim Umgang mit dynamisch generierten Inhalten sind gespeicherte Prozeduren genauso anfällig für Injektionsangriffe wie jede andere SQL-Interaktion. Sie können auch von den gleichen Taktiken profitieren, die auch bei vorbereiteten Anweisungen verwendet werden, insbesondere von der Verwendung von Platzhaltern und der Bereinigung der Eingaben vor der Übergabe an die Datenbank.
Wie der Name schon sagt, wird die gespeicherte Prozedur tatsächlich in der Datenbank selbst erstellt, während im Programm unmittelbar vor der Abfrage der Datenbank eine vorbereitete Anweisung eingerichtet wird. Wenn Sie bereits über eine gespeicherte Prozedur in der Datenbank verfügen, muss das Programm lediglich diese Prozedur aufrufen und dabei die Werte bereitstellen, die es zu empfangen erwartet.
Um die vorherige Beispiel-SQL-Abfrage zu verwenden, können Sie mit dem Befehl CREATE PROCEDURE wie folgt eine gespeicherte Prozedur in der Datenbank selbst erstellen:
|_+_|Jetzt verfügt Ihre Datenbank über eine gespeicherte Prozedur namens sp_getClientData. Um es zu nutzen, muss das Programm es nur aufrufen und einen Wert bereitstellen, der den Platzhalter ersetzt. Schauen Sie sich die Codebeispiele am Ende des Artikels zum Aufrufen einer gespeicherten Prozedur in jeder der behandelten Programmiersprachen an.
Tipp zur Serverhärtung: Zugriffsrechte mit den geringsten Privilegien
Der wichtigste Tipp, um den Datenbankserver selbst beim Umgang mit Programmen jeglicher Art etwas sicherer zu machen, besteht darin, die Zugriffsrechte aller verschiedenen „Benutzer“-Konten in der Datenbank zu minimieren. Es sollte nur ein DBA- oder Admin-Konto vorhanden sein und diese Anmeldeinformationen sollten niemals in einem Programm oder einer Anwendung verwendet werden. Tatsächlich sollte jedes Programm, das Zugriff auf eine Datenbank benötigt, über unterschiedliche Konten verfügen, je nachdem, welche Berechtigungen es beim Herstellen einer Verbindung benötigt.
Beim Erstellen dieser Konten kommt das uralte Sprichwort „Weniger ist mehr“ ins Spiel. Beginnen Sie ohne Rechte und fügen Sie dann nur die Rechte hinzu, die zum Ausführen der aufgerufenen Funktion erforderlich sind. Gleichzeitig müssen alle Ansichten oder gespeicherten Prozeduren gleichzeitig erstellt werden. Auch beim Thema Datensicherheit gilt: Weniger ist mehr.
Wenn das Programm nur Informationen zu einem Kunden suchen muss, benötigt es ein Benutzerkonto mit nur Lesezugriff auf die spezifischen Tabellen zu Kundeninformationen. Wenn es andererseits in der Lage sein soll, Informationen zu Mitarbeitern des Unternehmens zu ändern, muss das Benutzerkonto während des Datenbankverbindungssegments dieses Programms Lese- und Schreibzugriff auf Tabellen haben, die sich auf die Mitarbeiter des Unternehmens beziehen.
Codebeispiele
Lassen Sie uns all diese Informationen in einige tatsächliche Codebeispiele packen. Zunächst arbeiten wir mit einer MySQL-Datenbank auf dem Localhost namens „demo“. Der Einfachheit halber verfügt diese Datenbank nur über zwei Tabellen, Clients und Profile. Beachten Sie außerdem, dass das Betriebssystem für diese Beispiele Debian Linux ist, die Beispiele jedoch auch auf Servern mit anderen Betriebssystemen verwendet werden können.
Die Codebeispiele führen alle die gleichen Aufgaben aus, jedoch in unterschiedlichen Programmiersprachen. Die Sprachen sind (in keiner bestimmten Reihenfolge) Java, PHP, Python und Perl.
Eine letzte Anmerkung. In den folgenden Beispielen läuft der Prozess für unsere Programme folgendermaßen ab:
- Stellen Sie eine Verbindung zur Datenbank her
- Bereiten Sie eine Anweisung mit einem oder mehreren Platzhaltern vor oder rufen Sie eine gespeicherte Prozedur auf
- Erhalten Sie die erforderlichen Eingaben vom Benutzer
- Bereinigen Sie die Eingaben des Benutzers
- Fügen Sie die Benutzereingaben in die SQL-Transaktion ein und ersetzen Sie sie durch Platzhalter.
- Führen Sie die SQL-Abfrage aus
- Zeigen Sie die Ergebnisse der SQL-Abfrage an
- Trennen Sie die Verbindung zur Datenbank, wenn Sie fertig sind
Java
In Java ist es ziemlich einfach, Eingaben von einem Benutzer zu erhalten:
|_+_|Um eine Verbindung zu einer Datenbank herzustellen, muss das Programm die Adresse des Servers, den Namen der Datenbank auf diesem Server und die Anmeldeinformationen eines Kontos kennen, das über die erforderlichen Zugriffsrechte für die auszuführenden SQL-Befehle verfügt während seiner Datenbankverbindungssitzung:
|_+_|Um eine parametrisierte Anweisung vorzubereiten, benötigen Sie den Platzhalter in der SQL-Anweisung, die an die Datenbank übergebene Anweisung und den vom Benutzer bereitgestellten Wert, der anstelle des Platzhalters bereitgestellt wird:
String query = 'SELECT * FROM client WHERE clientID = ?