Übersicht
In einem relationalen Datenbankverwaltungssystem (RDBMS) gibt es eine bestimmte Sprache – genannt SQL (Structured Query Language) – die zur Kommunikation mit der Datenbank verwendet wird. Die in SQL geschriebenen Abfrageanweisungen werden verwendet, um den Inhalt und die Struktur der Datenbank zu manipulieren. Eine bestimmte SQL-Anweisung, die die Struktur der Datenbank erstellt und ändert, wird als DDL-Anweisung (Data Definition Language) bezeichnet, und die Anweisungen, die den Inhalt der Datenbank bearbeiten, werden als DML-Anweisung (Data Manipulation Language) bezeichnet. Die mit dem RDBMS-Paket verknüpfte Engine analysiert und interpretiert die SQL-Anweisung und gibt das Ergebnis entsprechend zurück. Dies ist der typische Prozess der Kommunikation mit RDBMS – eine SQL-Anweisung auslösen und das Ergebnis zurückerhalten, das ist alles. Das System beurteilt nicht die Absicht einer Aussage, die der Syntax und semantischen Struktur der Sprache entspricht. Das bedeutet auch, dass es keine Authentifizierungs- oder Validierungsprozesse gibt, um zu überprüfen, wer die Anweisung ausgelöst hat und welche Rechte man hat, um die Ausgabe zu erhalten. Ein Angreifer kann einfach eine SQL-Anweisung mit böswilliger Absicht abfeuern und Informationen zurückerhalten, die er nicht erhalten sollte. Beispielsweise kann ein Angreifer eine SQL-Anweisung mit böswilliger Nutzlast mit der harmlos aussehenden Abfrage ausführen, um den Datenbankserver einer Webanwendung zu kontrollieren.
Wie es funktioniert
Ein Angreifer kann diese Schwachstelle ausnutzen und zum eigenen Vorteil nutzen. Beispielsweise kann man den Authentifizierungs- und Autorisierungsmechanismus einer Anwendung umgehen und sogenannte sichere Inhalte aus der gesamten Datenbank abrufen. Eine SQL-Injection kann verwendet werden, um Datensätze aus der Datenbank zu erstellen, zu aktualisieren und zu löschen. Man kann also mit SQL eine auf die eigene Vorstellung beschränkte Abfrage formulieren.
Typischerweise sendet eine Anwendung häufig SQL-Abfragen an die Datenbank für zahlreiche Zwecke, sei es zum Abrufen bestimmter Datensätze, Erstellen von Berichten, Authentifizieren von Benutzern, CRUD-Transaktionen und so weiter. Der Angreifer muss lediglich eine SQL-Eingabeabfrage in einem Anwendungseingabeformular finden. Die vom Formular vorbereitete Abfrage kann dann verwendet werden, um den bösartigen Inhalt so zu verzwirbeln, dass, wenn die Anwendung die Abfrage auslöst, diese auch die eingeschleuste Nutzlast enthält.
Eine der idealen Situationen ist, wenn eine Anwendung den Benutzer um Eingaben wie Benutzername oder Benutzer-ID bittet. Die Anwendung öffnete dort eine Schwachstelle. Die SQL-Anweisung kann unwissentlich ausgeführt werden. Ein Angreifer nutzt dies aus, indem er eine Nutzlast einschleust, die als Teil der SQL-Abfrage verwendet und von der Datenbank verarbeitet wird. Beispielsweise könnte der serverseitige Pseudocode für eine POST-Operation für ein Anmeldeformular wie folgt lauten:
uname = getRequestString("username"); pass = getRequestString("passwd"); stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "'"; database.execute(stmtSQL);
Der vorangehende Code ist anfällig für SQL-Injection-Angriffe, da die Eingaben an die SQL-Anweisung durch die Variablen „uname“ und „pass“ auf eine Weise manipuliert werden können, die die Semantik der Anweisung verändern würde.
Beispielsweise können wir die Abfrage so ändern, dass sie gegen den Datenbankserver läuft, wie in MySQL.
stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "' OR 1=1";
Dies führt dazu, dass die ursprüngliche SQL-Anweisung so weit modifiziert wird, dass die Authentifizierung umgangen werden kann. Dies ist eine ernsthafte Schwachstelle und muss innerhalb des Codes verhindert werden.
Abwehr gegen einen SQL-Injection-Angriff
Eine Möglichkeit, die Wahrscheinlichkeit eines SQL-Injection-Angriffs zu verringern, besteht darin, sicherzustellen, dass die ungefilterten Textzeichenfolgen vor der Ausführung nicht an die SQL-Anweisung angehängt werden dürfen. Beispielsweise können wir PreparedStatement verwenden um erforderliche Datenbankaufgaben auszuführen. Der interessante Aspekt von PreparedStatement ist, dass es statt einer Zeichenfolge eine vorkompilierte SQL-Anweisung an die Datenbank sendet. Das bedeutet, dass Abfrage und Daten getrennt an die Datenbank gesendet werden. Dies verhindert die eigentliche Ursache des SQL-Injection-Angriffs, denn bei der SQL-Injection besteht die Idee darin, Code und Daten zu mischen, wobei die Daten tatsächlich ein Teil des Codes in Form von Daten sind. In PreparedStatement , gibt es mehrere setXYZ() Methoden wie setString() . Diese Methoden werden zum Filtern von Sonderzeichen wie Zitaten verwendet, die in den SQL-Anweisungen enthalten sind.
Beispielsweise können wir eine SQL-Anweisung auf folgende Weise ausführen.
String sql = "SELECT * FROM employees WHERE emp_no = "+eno;
Anstatt beispielsweise eno=10125 zu setzen B. eine Mitarbeiternummer in der Eingabe, können wir die Abfrage mit der Eingabe ändern, z. B.:
eno = 10125 OR 1=1
Dadurch wird das von der Abfrage zurückgegebene Ergebnis vollständig geändert.
Ein Beispiel
Im folgenden Beispielcode haben wir gezeigt, wie PreparedStatement kann verwendet werden, um Datenbankaufgaben auszuführen.
package org.mano.example; import java.sql.*; import java.time.LocalDate; public class App { static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; static final String DB_URL = "jdbc:mysql://localhost:3306/employees"; static final String USER = "root"; static final String PASS = "secret"; public static void main( String[] args ) { String selectQuery = "SELECT * FROM employees WHERE emp_no = ?"; String insertQuery = "INSERT INTO employees VALUES (?,?,?,?,?,?)"; String deleteQuery = "DELETE FROM employees WHERE emp_no = ?"; Connection connection = null; try { Class.forName(JDBC_DRIVER); connection = DriverManager.getConnection (DB_URL, USER, PASS); }catch(Exception ex) { ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(insertQuery);){ pstmt.setInt(1,99); pstmt.setDate(2, Date.valueOf (LocalDate.of(1975,12,11))); pstmt.setString(3,"ABC"); pstmt.setString(4,"XYZ"); pstmt.setString(5,"M"); pstmt.setDate(6,Date.valueOf(LocalDate.of(2011,1,1))); pstmt.executeUpdate(); System.out.println("Record inserted successfully."); }catch(SQLException ex){ ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(selectQuery);){ pstmt.setInt(1,99); ResultSet rs = pstmt.executeQuery(); while(rs.next()){ System.out.println(rs.getString(3)+ " "+rs.getString(4)); } }catch(Exception ex){ ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(deleteQuery);){ pstmt.setInt(1,99); pstmt.executeUpdate(); System.out.println("Record deleted successfully."); }catch(SQLException ex){ ex.printStackTrace(); } try{ connection.close(); }catch(Exception ex){ ex.printStackTrace(); } } }
Ein Blick in PreparedStatement
Diese Jobs können auch mit einem JDBC Statement ausgeführt werden Schnittstelle, aber das Problem ist, dass es manchmal ziemlich unsicher sein kann, besonders wenn eine dynamische SQL-Anweisung ausgeführt wird, um die Datenbank abzufragen, wo Benutzereingabewerte mit den SQL-Abfragen verkettet werden. Wie wir gesehen haben, kann dies eine gefährliche Situation sein. In den meisten Fällen Statement ist ganz harmlos, aber PreparedStatement scheint die bessere Option zwischen den beiden zu sein. Es verhindert, dass böswillige Zeichenfolgen verkettet werden, da es einen anderen Ansatz zum Senden der Anweisung an die Datenbank hat. PreparedStatement verwendet Variablensubstitution statt Verkettung. Das Setzen eines Fragezeichens (?) in der SQL-Abfrage bedeutet, dass eine Ersatzvariable an deren Stelle tritt und den Wert liefert, wenn die Abfrage ausgeführt wird. Die Position der Substitutionsvariablen findet entsprechend der zugewiesenen Parameterindexposition im setXYZ() statt Methoden.
Diese Technik schützt vor SQL-Injection-Angriffen.
Außerdem PreparedStatement implementiert AutoCloseable. Dadurch kann es im Kontext eines try-with-resources schreiben blockieren und automatisch schließen, wenn es den Gültigkeitsbereich verlässt.
Schlussfolgerung
Ein SQL-Injection-Angriff kann nur durch verantwortungsvolles Schreiben des Codes verhindert werden. Tatsächlich wird die Sicherheit in jeder Softwarelösung meistens durch schlechte Codierungspraktiken verletzt. Hier haben wir beschrieben, was zu vermeiden ist und wie PreparedStatement kann uns beim Schreiben von sicherem Code helfen. Eine vollständige Vorstellung von SQL-Injection finden Sie in den entsprechenden Materialien; das Internet ist voll davon, und für PreparedStatement , sehen Sie in der Java-API-Dokumentation nach, um eine ausführlichere Erklärung zu erhalten.