(Anwendungssicherheit: SQL Injektion verhindern von Adrian Janotta)

Wir haben nun bereits einiges über Anwendungssicherheit disikutiert und bevor wir in das Thema Netzwerksicherheit übergehen, sollten wir noch einige Details der Anwendungssicherheit besprechen. Das Thema SQL-Injection ist bereits alt und den SQL oder SQLi Injektion Angriff auf Anwendungen gibt es bereits seitdem es Anwendungen gibt die programmiert worden sind. Mit SQL-Injetion bezeichnet man Angriffe auf einen SQL-Query bei der Werte eingeschleust werden, die vom Server-Betreiber eigentlich so nicht vorgesehen waren. Unter Umständen erhält ein Angreifer direkten zugriff auf die Datenbanken, also dem gesamten Schatz, den es eigentlich zu schützen gilt. Mit einem SQL-Injetion Angriff ist es unter anderem möglich Daten auszuspähen, in dem Sinn zu verändern, die Kontrolle über den Server zu erhalten oder einfach größtmöglichen Schaden anzurichten.

SQL Injection verhindern - Webseiten Sicherheit mit Janotta und Partner.

Viele Entwickler sind sich nicht bewusst, wie man sich an SQL Abfragen zu schaffen machen kann und nehmen an, dass eine SQL Abfrage ein vertrauenswürdiges Kommando ist. Das heißt, dass SQL Abfragen Zugriffskontrollen hinters Licht führen, und dadurch Standard Authentifizierungs- und Authorisationschecks umgehen können, und manchmal können SQL Abfragen sogar Zugriff zu Kommandos auf Betriebssystemebene erlauben. Es existieren aber weitaus mehr Angriffsarten auf die SQL Datenbanken, zum Beispiel wären da noch der Direkt SQL Command Injection. Das ist eine Technik, wo ein Angreifer SQL Kommandos erstellt oder existierende verändert, um versteckte Daten sichtbar zu machen, wertvolle Daten zu überschreiben, oder sogar gefährliche Kommandos auf Systemebene des Datenbank-Hosts auszuführen. Dies wird durch die Applikation erreicht, welche den Input des Benutzers mit statischen Parametern kombiniert, um eine SQL Abfrage zu erstellen. Die folgenden Beispiele basieren auf wahren Begebenheiten.

Beispiel Angriff auf das Betriebssystem des Datenbank Hosts (Microsoft SQL Server)


$query  = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);

SQL Injection, SQLi Injection und weitere Datenbank Angriffe unterbinden

Obwohl es offensichtlich ist, dass ein Angreifer zumindest ein wenig Kenntnis der genutzten Datenbankarchitektur besitzen muss, um einen erfolgreichen Angriff durchzuführen, ist das Erlangen dieser Informationen oft sehr einfach. Wenn die Datenbank zum Beispiel Teil eines Open Source oder anderweitig öffentlich verfügbaren Paketes mit einer Standard Installation ist, dann ist diese Information vollkommen frei zugänglich. Diese Information kann auch durch Closed Source Code – selbst wenn dieser kodiert, verschleiert oder kompiliert ist – und sogar durch ihren ureigenen Code durch die Anzeige von Fehlermeldungen. Andere Methoden beinhalten die Nutzung typischer Tabellen und Spalten Namen. Ein Login Formular etwa, dass eine Tabelle ‚users‘ mit den Spaltennamen ‚id‘, ‚username‘ und ‚password‘ nutzt.

Diese Angriffe basieren hauptsächlich auf dem Ausnutzen des Codes, welcher ohne Bedenken auf die Sicherheit geschrieben wurde. Vertrauen Sie nie auf irgendeine Art von Input, speziell wenn er von der Clientseite kommt, selbst wenn er von einer Auswahlbox, einem versteckten Eingabefeld, oder einem Cookie kommt. Das erste Beispiel zeigt, dass solch eine untadelige Abfrage ein Disaster anrichten kann.

1. Stellen Sie nie als Superuser oder Owner einer Datenbank eine Verbindung zur Datenbank her. Verwenden Sie immer speziell angelegte Benutzer mit sehr limitierten Rechten.

2. Verwenden Sie Prepared Statements mit gebundenen Variablen. Sie werden von PDO, MySQLi und anderen Bibliotheken angeboten.

3. Prüfen Sie, ob der gegebene Input dem erwarteten Datentyp entspricht. PHP bietet eine große Auswahl an Funktionen zum Validieren des Input, von den einfachsten unter Variablenfunktionen und Character Type Functions (z.B. is_numeric() bzw. ctype_digit()), bis hin zu den Perl kompatiblen Regulären Ausdrücken.

Wenn die Applikation numerischen Input erwartet, erwägen Sie die Prüfung der Daten mit ctype_digit(), oder die Änderung des Typs mit settype(), oder verwenden Sie die numerische Repräsentation mittels sprintf.

Ein sicherer Weg, eine Abfrage zu erstellen


settype($offset, 'integer');
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
/ Beachten Sie %d im Formatstring, %s zu verwenden wäre sinnlos
$query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
$offset);
1. Unterstützt der Datenbank-Layer das Binden von Variablen nicht, so maskieren Sie jede nicht numerische Benutzereingabe, welche zur Datenbank weitergereicht werden soll mit der jeweiligen datenbankspezifischen Escape-Funktion (z.B. mysql_real_escape_string(), sqlite_escape_string() usw.). Generische Funktionen wie addslashes() sind nur in einer sehr spezifischen Umgebung nüztlich (z.B. MySQL mit einem Single Byte Zeichensatz bei deaktiviertem NO_BACKSLASH_ESCAPES), so dass es besser ist diese zu vermeiden.2. Geben Sie keinerlei datenbankspezifische Informationen aus, speziell über das Schema, egal wie (auf ehrliche oder unehrliche Weise). Siehe auch Fehlerbehandlung und Error Handling and Logging Functions.

3. Sie können stored procedures und vorher definierte Cursor verwenden, um den Datenzugriff zu abstrahieren, sodass Benutzer nicht direkt auf Tabellen oder Views zugreifen, aber diese Lösung hat andere Auswirkungen.

Abgesehen davon profitieren Sie von einer Protokollierung der Abfragen entweder in Ihrem Skript, oder durch die Datenbank selbst, wenn es hilft. Klar, die Protokollierung kann nicht irgendeinen schädlichen Versuch verhindern, aber es kann helfen herauszufinden, welche Applikation umgangen wurde. Das Log ist durch die in ihm enthaltene Information nützlich, und je mehr Details es enthält, desto besser ist es im Allgemeinen.

In Python gibt es mehrere Möglichkeiten mit einer Datenbank zu kommunizieren. Eine davon ist SQLAlchemy. Um hierbei SQL Injection zu entgehen, sollte man rohe SQL Befehle vermeiden, sofern dieser durch Form- oder URL Anfragen manipuliert werden könnten. Nachfolgend ist ein solches Beispiel zu sehen:

db.engine.execute("SELECT * FROM table")

Stattdessen wird dazu geraten die internen Funktionen und Methoden von SQLAlchemy zu nutzen, wie z.B. die Folgenden

session.query(Order).get(order_id)
session.query(Order).filter(Order.status == 'active')

Visual Basic (ADOdb)

In Visual Basic gibt es einfache Command-Objekte, mit denen diese Probleme vermieden werden können.

Anstatt

cn.Execute "SELECT spalte1 FROM tabelle WHERE spalte2 = '" & spalte2Wert & "'"

sollte Folgendes verwendet werden:

Dim cmd As ADODB.Command, rs as ADODB.Recordset
With cmd
  Set .ActiveConnection = cn
  Set .CommandType = adCmdText
  .CommandText = "SELECT spalte1 FROM tabelle WHERE spalte2 = ?"
  .Parameters.Append .CreateParameter("paramSp2", adVarChar, adParamInput, 25, spalte2Wert) '25 ist die max. länge
  Set rs = .Execute
End With

Microsoft .NET Framework – C# (ADO.NET)

Im .NET Framework gibt es einfache Objekte, mit denen solche Probleme umgangen werden können.

Anstatt

SqlCommand cmd = new SqlCommand("SELECT spalte1 FROM tabelle WHERE spalte2 = '"
                 + spalte2Wert + "';");

sollte Folgendes verwendet werden:

string spalte2Wert = "Mein Wert";
SqlCommand cmd = new SqlCommand("SELECT spalte1 FROM tabelle WHERE spalte2 = @spalte2Wert;");
cmd.Parameters.AddWithValue("@spalte2Wert", spalte2Wert);

Java (JDBC)

Eine SQL-Injection kann leicht durch eine bereits vorhandene Funktion verhindert werden. In Java wird zu diesem Zweck die PreparedStatement-Klasse verwendet (JDBC-Technologie) und die Daten unsicherer Herkunft werden als getrennte Parameter übergeben. Um die Daten von der SQL-Anweisung zu trennen wird der Platzhalter „?“ verwendet.

Anstatt

Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery("SELECT spalte1 FROM tabelle WHERE spalte2 = '"
                 + spalte2Wert + "';");

sollte Folgendes verwendet werden:

PreparedStatement pstmt = con.prepareStatement("SELECT spalte1 FROM tabelle WHERE spalte2 = ?");
pstmt.setString(1, spalte2Wert);
ResultSet rset = pstmt.executeQuery();

Der Mehraufwand an Schreibarbeit durch die Verwendung der PreparedStatement-Klasse kann sich außerdem durch einen Performancegewinn auszahlen, wenn das Programm das PreparedStatement-Objekt mehrfach verwendet.

PHP

In PHP steht für Datenbankzugriffe die Bibliothek PHP Data Objects zur Verfügung.

Beispiel ohne Prepared Statement:

$dbh->exec("INSERT INTO REGISTRY (name, value)
            VALUES (".$dbh->quote($name,PDO::PARAM_STR).", ".$dbh->quote($value,PDO::PARAM_INT).")");

Beispiel mit Prepared Statement:

$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':value', $value);

Bis zur PHP-Version 5.3 gab es die Konfigurationsoption „magic_quotes_gpc“. War diese auf „on“ gestellt, wurden von außen kommende Benutzereingaben automatisch maskiert. Manche Skripte nutzen Funktionen wie etwa addslashes() oder mysql_real_escape_string(). Das heißt, dass bereits allen relevanten Zeichen in den Benutzereingaben durch so genannte Magic Quotes ein Backslash vorangestellt wurde und nun durch die Escape-Funktion erneut ein Backslash vorangestellt wird. Somit werden die Benutzereingaben verfälscht und man erhält beispielsweise anstatt eines einfachen Anführungszeichens ein Anführungszeichen mit vorangestelltem Backslash (\"). Auch aus Gründen der Portabilität sollte bei der Entwicklung von Anwendungen auf diese Einstellung verzichtet und stattdessen alle Eingaben manuell validiert und maskiert werden, da nicht davon ausgegangen werden kann, dass auf allen Systemen dieselben Einstellungen vorherrschen oder möglich sind. Darüber hinaus sollte addSlashes() nicht zum Maskieren von Datenbank-Eingaben benutzt werden, da es keine ausreichende Sicherheit gegenüber mysql_real_escape_string() gewährleistet.

Nach der PHP-Version 5.3 wurde mysql_real_escape_string() durch MySQLi ersetzt. Ab der Version 7.0 ist mysql_real_escape_string() nicht mehr verfügbar.

Perl

Mit dem datenbankunabhängigen Datenbankmodul DBI, welches normalerweise in Perl verwendet wird:

Anstatt

$arrayref = $databasehandle->selectall_arrayref("SELECT spalte1 FROM tabelle WHERE spalte2 = $spalte2Wert");

sollte Folgendes verwendet werden:

$arrayref = $databasehandle->selectall_arrayref('SELECT spalte1 FROM tabelle WHERE spalte2 = ?',{},$spalte2Wert);

Perls DBI-Modul unterstützt außerdem eine „prepare“-Syntax ähnlich der aus dem Java-Beispiel.

$statementhandle = $databasehandle->prepare("SELECT spalte1 FROM tabelle WHERE spalte2 = ?");
$returnvalue = $statementhandle->execute( $spalte2Wert );

Alternativ können über das Datenbankhandle auch Eingabe-Werte sicher maskiert werden, dabei achtet der DB-Treiber auf die für diese Datenbank typischen Sonderzeichen, der Programmierer muss keine tiefergehenden Kenntnisse darüber haben.

$arrayref = $databasehandle->selectall_arrayref("SELECT spalte1 FROM tabelle WHERE spalte2 = " .
						$databasehandle->quote($spalte2Wert) );

Im sogenannten „tainted mode“ verwendet Perl starke Heuristiken, um nur sichere Zugriffe zu erlauben. Zeichenketten, die vom Benutzer übergebene Parameter enthalten werden zunächst als „unsicher“ behandelt, bis die Daten explizit validiert wurden, und dürfen vorher nicht in unsicheren Befehlen verwendet werden.

ColdFusion Markup Language

Unter ColdFusion kann das <cfqueryparam>-Tag verwendet werden, welches sämtliche notwendigen Validierungen übernimmt:

   SELECT * FROM courses WHERE Course_ID =
   <cfqueryparam value = "#Course_ID#" CFSQLType = "CF_SQL_INTEGER">

MS-SQL

Über parametrisierte Kommandos kann die Datenbank vor SQL-Injections geschützt werden:

SELECT COUNT(*) FROM Users WHERE UserName=? AND UserPasswordHash=?