SQL-Injection beheben: Ein ORM reicht nicht aus
8. Juni 2016
0 Min. LesezeitEine der gefährlichsten und am weitesten verbreiteten Arten von Schwachstellen ist die SQL-Injection. Sie ermöglicht Angreifern den Zugriff auf Ihre Backend-Datenbank. Prepared Statements und Object-Relational Mapping (ORM) sind eine gute Möglichkeit, sich vor SQL-Injection zu schützen, reichen aber nicht aus. Wie dieser Beitrag zeigt, können ORM-Pakete wie Sequelize und MySQL Schwachstellen aufweisen, durch die Sie angreifbar werden. Für umfassenden Schutz müssen Sie mehr tun.
Unserem State of Open Source Security Report 2019 zufolge sind SQL-Injection-Schwachstellen nach wie vor eine häufige Ursache für Sicherheitsbedenken. In Bibliotheken im PHP-Packagist-Repository wurden bis zu 16 Schwachstellen gefunden.
Kurzfassung
Eine SQL-Injection-Schwachstelle, kurz SQLi, ermöglicht es Angreifern, unstrukturierten Text in einen SQL-Befehl einzufügen und so unbeabsichtigte Folgen auszulösen. Eine Möglichkeit, sich vor SQL-Injection zu schützen, ist die Verwendung eines ORM-Pakets. Es ordnet Ihre Objekte und die darauf ausgeführten Aktionen SQL zu. Mit solchen Paketen müssen Sie SQL nicht selbst zusammensetzen und können daher auch nicht versehentlich schädliche Inhalte einschleusen. Hurra!
Leicht zu vergessen ist jedoch: Nur weil Sie SQL nicht selbst zusammensetzen, heißt das nicht, dass es nicht zusammengesetzt wird. Tatsächlich müssen die ORM-Pakete in npm diese Aktionen weiterhin in SQL umwandeln. Wie alle Pakete sind auch diese Software – und Software kann Fehler enthalten. Im vergangenen Jahr wurden bei den beiden führenden ORM-Paketen in npm, sequelize und node-mysql, 4 SQL-Injection-Schwachstellen gemeldet. Damit wurde aus einem theoretischen Risiko Realität. Diese Probleme sind in den neuesten Paketversionen behoben. Möglicherweise verwenden Sie jedoch ältere Versionen, und es können neue Schwachstellen auftreten.
Dieser Beitrag zeigt einige Beispiele für solche Schwachstellen und erklärt, welche weiteren Schutzebenen Sie einsetzen sollten. Dazu gehören insbesondere:
Testen Sie Ihre Abhängigkeiten auf Schwachstellen
Validieren Sie Eingaben, auch wenn Sie ein ORM verwenden
Bevor wir uns die problematischen Szenarien ansehen, frischen wir kurz auf, was SQL-Injection ist, welche Möglichkeiten es gibt, sich davor zu schützen, und was ein ORM ist.
Ein einfaches Beispiel für SQL-Injection
Stellen Sie sich eine To-do-Listen-App mit einer Suchfunktion vor, die nach Einträgen mit einem bestimmten Text sucht. Die Suche erfolgt über eine Datenbankabfrage, die mit dem Paket sequelize implementiert wurde. Wenn die Datenbankverbindung bereits eingerichtet ist, könnte die Backend-Suchfunktion so aussehen:
In einem legitimen Anwendungsfall gibt der Benutzer einen einfachen Wert ein, zum Beispiel Buy. Daraus ergibt sich die folgende legitime Abfrage, die alle relevanten Einkaufsaufgaben in unserer Liste zurückgibt:
Ein Angreifer könnte jedoch absichtlich ein einfaches Anführungszeichen (') eingeben, um den vorgesehenen Zeichenfolgenkontext zu verlassen und direkt in die Abfrage einzudringen. Der Angreifer könnte beispielsweise den Wert ') UNION SELECT username||'_'||password FROM Users -- eingeben. Daraus ergäbe sich folgende Abfrage:
Angenommen, wir haben eine Tabelle Users mit den Feldern password und username. Dann fügt die obige Abfrage unserer TODO-Liste die vollständige Liste mit Benutzernamen und Passwörtern hinzu. Der restliche Text lässt sich einfach mit dem Befehl -- auskommentieren, damit die SQL-Abfrage intakt bleibt.
Diese anfällige Implementierung mag besonders naiv wirken, doch Varianten davon kommen recht häufig vor. In allen Fällen wird eine Form ungeprüfter Benutzereingabe an eine rohe SQL-Abfrage angehängt. Dadurch wird der ursprüngliche Kontext (zum Beispiel eine Zeichenfolge) verlassen und es werden unbeabsichtigte Aktionen ausgelöst. Der von uns gezeigte Angriff war außerdem recht einfach und präzise. Angreifer senden in der Praxis jedoch nach und nach viele verschiedene Angriffsvarianten. Nur eine davon muss erfolgreich sein.
Auswirkungen und Behebung
SQL-Injection ist eine äußerst schwerwiegende Schwachstelle. In den meisten Fällen lässt sich eine einzelne SQL-Injection an beliebiger Stelle Ihrer Website letztlich dazu ausweiten, beliebige Abfragen auf der Datenbank auszuführen und deren Daten auszulesen oder zu verändern. Da Datenbanken häufig die sensibelsten Informationen eines Systems enthalten, sind solche Zugriffsmöglichkeiten für Angreifer verheerend.
Es gibt zwei zentrale, miteinander kombinierbare Techniken zur Verhinderung von SQL-Injection: Eingabevalidierung und Prepared Statements.
Eingabevalidierung
SQL-Injection beginnt wie andere Injection-Angriffe mit einer schädlichen Benutzereingabe. Eine gute Möglichkeit, sie zu verhindern, besteht daher darin, die Gültigkeit der Eingabe zu überprüfen. Ist sie ungültig, können wir den Vorgang vollständig abbrechen oder potenziell gefährliche Zeichen entfernen.
Die Eingabevalidierung kann mit einem negativen oder einem positiven Sicherheitsmodell erfolgen.
Beim negativen Sicherheitsmodell werden bestimmte gefährliche Zeichen oder Muster verboten. In unserem Beispiel könnten wir etwa das einfache Anführungszeichen blockieren, durch das der Zeichenfolgenkontext verlassen wurde. Leider ist SQL komplex, sodass es schwierig ist, alle potenziell gefährlichen Zeichen zu finden. In diesem Beispiel müssten wir außerdem Zeichen wie Rückschritt (b), Backslash (\), Nullzeichen (x00) und wahrscheinlich noch einige weitere blockieren. Unterstützen wir mehrere Datenbanktypen, wird die Liste noch länger. Dennoch ist das Blockieren gefährlicher Zeichen eine relativ wirksame und einfache Gegenmaßnahme.
Beim positiven Sicherheitsmodell sind nur bestimmte Zeichen erlaubt. In unserem Beispiel hätte eine Beschränkung der Eingabe auf Buchstaben, Ziffern und Leerzeichen (/a-zA-Z0-9 /) das Risiko wirksam beseitigt. Dieser Ansatz, auch Allowlisting genannt, ist aus Sicherheitssicht meist besser, da unerwartete Werte ausgeschlossen werden. Allerdings werden dadurch eher auch zulässige Zeichen blockiert, insbesondere wenn Unicode-Zeichensätze unterstützt werden.
Prepared Statements und ORM
Beginnen SQL-Injection-Angriffe mit der Eingabe, enden sie mit der SQL-Abfrage. Dort bietet sich also die zweite Gelegenheit, das Problem zu verhindern. Die Ursache ist in diesem Fall die einfache Verkettung von Zeichenfolgen, mit der die SQL-Abfrage erstellt wird. Würden wir stattdessen eine Vorlage für die Abfrage verwenden, könnten wir der Datenbank (oder der verbindenden Bibliothek) mitteilen, dass es sich um einen Zeichenfolgenwert handelt, den sie bei Bedarf entsprechend kodieren soll. Genau diese Lösung haben wir zu Beginn des Beitrags erwähnt.
Diese Vorlagen sind als „Prepared Statements“ oder manchmal auch als „parametrisierte Anweisungen“ bekannt. Auch das oben verwendete Paket sequelize unterstützt sie. Wir könnten die Schwachstelle also beheben, indem wir unsere Funktion folgendermaßen ändern:
Sequelize erkennt, dass ? für einen Wert und nicht für einen SQL-Befehl steht, kodiert ihn entsprechend und verhindert so Versuche, aus dem vorgesehenen Kontext auszubrechen.
Prepared Statements sind nicht nur eine gute Möglichkeit, Ihren Code abzusichern, sondern machen ihn auch lesbarer und leichter wartbar. Anders gesagt: Wenn Sie versucht sind, Werte an eine SQL-Anweisung anzuhängen, widerstehen Sie dem Impuls und verwenden Sie stattdessen ein Prepared Statement.
Einen Schritt weiter geht der noch stärker programmatische Ansatz für SQL mit Object-Relational Mapping (ORM). Bei der Verwendung eines ORM werden Ihre Datenbanktabellen Ihren Objekten zugeordnet, sodass Sie ganze Objekte lesen, schreiben und abfragen können. Da Sie mit ORM weniger direktes SQL verwenden, ist es ebenfalls eine gute Möglichkeit, SQL-Injection zu vermeiden.
Wenn ORMs anfällig sind: Sequelize
Prepared Statements und ORM sind beide gute Möglichkeiten, die Verantwortung für die Kodierung den „Experten“ zu überlassen – also den Paketen, die genau darauf spezialisiert sind. Doch auch Experten machen Fehler … Wie eingangs erwähnt, wurde dies im vergangenen Jahr durch 4 SQL-Injection-Schwachstellen in zwei der führenden ORM-npm-Pakete, sequelize und node-mysql, deutlich.
Die Schwachstellen waren durchweg auf nicht validierte Parameter in verschiedenen ORM-Aufrufen und Aufrufen von Prepared Statements zurückzuführen. Auch diese Funktionen müssen die Parameter letztlich in eine SQL-Anweisung umwandeln und können dabei vergessen, sie zu escapen oder zu validieren.
Sehen wir uns einige der sequelize-Schwachstellen an, um besser zu verstehen, was passiert ist. Diese Schwachstellen sind inzwischen behoben. Die Sequelize-Entwickler reagierten nach der Meldung der Probleme schnell. Die vollständige Liste der Sequelize-Schwachstellen finden Sie in Snyks Vulnerability DB, zusammen mit Hinweisen zur Behebung.
Die erste Schwachstelle wurde im Januar 2016 offengelegt und in Version 3.17.0 behoben. Sie betraf die Funktion findAll, die bei der Verwendung eines ORM häufig zum Abfragen von Objekten aus der Datenbank eingesetzt wird. Bei der Umwandlung ihrer Parameter in SQL schränkte die Funktion die Werte des LIMIT-Parameters nicht ein und ermöglichte so eine SQL-Injection-Schwachstelle.
Hier sehen Sie ein Beispiel dafür, wie diese Schwachstelle bei einer Liste von TODO-Elementen ausgenutzt werden könnte.
Angenommen, Items enthält die Felder Username und Desc. Die resultierende Abfrage würde ungefähr so aussehen:
Ein ähnliches Versäumnis wurde Ende März 2016 offengelegt. Diesmal lag der Fehler beim Erstellen von Prepared Statements und beim Verketten von Werten für die IN-Anweisung. Hier ist ein Beispiel für einen Angriff:
Diese Schwachstellen sind nach ihrer Entdeckung nicht besonders komplex zu beheben. Sie zeigen jedoch, dass auch beliebte Pakete nicht unfehlbar sind. SQL ist komplex, und Randfälle werden leicht übersehen.
Lösung: Defense in Depth
Falls es noch nicht klar war, möchte ich es ganz deutlich sagen: Sie sollten unbedingt ORM und Prepared Statements verwenden. Sie beseitigen den Großteil des SQL-Injection-Risikos und gehören generell zu den guten Softwarepraktiken. Sie sollten jedoch nicht davon ausgehen, dass Sie durch die Verwendung dieser Pakete vollständig immun sind.
Stattdessen sollten Sie auch Eingaben validieren. So gelangen schädliche Eingaben gar nicht erst in Ihr System – eine hervorragende Möglichkeit, das Risiko zu senken. Der Einsatz mehrerer solcher Schutzebenen wird oft als „Defense in Depth“ bezeichnet und ist sehr sinnvoll. Dringt ein Angriff durch eine Schutzebene (Eingabevalidierung), blockiert ihn wahrscheinlich die zweite Ebene. Wenn jede Ebene 1 % der Angriffe durchließe, würden zwei Ebenen bis auf 0,01 % alle Angriffe abwehren – das ist gute Mathematik.
Behalten Sie außerdem bekannte Schwachstellen in den von Ihnen verwendeten Paketen im Blick und beheben Sie sie, sobald Sie davon erfahren. Mit Snyk können Sie Ihre Anwendungen kostenlos auf die oben genannten Schwachstellen testen und Schwachstellentests in Ihren Entwicklungsprozess integrieren, um Ihre Anwendungen frei von Schwachstellen zu halten.
Von Entwicklern geschätzt. Von der Security vertraut.
Die Developer-first-Tools von Snyk bieten integrierte und automatisierte Security, die Ihren Governance- und Compliance-Anforderungen gerecht wird.
