17.03 Sicherheit bei der Arbeit mit Datenbanken

Im letzten Kapitel haben Sie die Grundoperationen in der Java HSQLDB-API kennen gelernt. Doch ein erfahrener Programmierer würde Ihren Code anschauen und mit dem Kopf schütteln, da Sie einige Dinge missachtet haben, die beim Umgang mit Datenbanken wichtig sind.

Passwörter in der Datenbank

Wenn Sie ein Programm mit Nutzerverwaltung schreiben, ist es unumgänglich, dass irgendwo die Passwörter der Benutzer gespeichert werden. Ein guter Ort dafür ist die Datenbank, und, sie werden es nicht glauben, sogar die selbe Tabelle wie die der Benutzer. Allerdings dürfen Sie Passwörter auf keinen Fall unverschlüsselt speichern. Im Internet ist es nämlich relativ einfach, Datenbanken zu knacken, man muss nur in die Presse schauen, um zu sehen, dass selbst große Firmen Probleme damit haben. Wenn das Programm als Desktoprechner gebraucht wird, brauchen Sie nur ein halbwegs computervisiertes Familienmitglied, um an die Datenbank zu kommen.
Also Passwort verschlüsseln. Aber wie? Natürlich können Sie das Ceaser-Chiffre oder das Enigma-Chiffre verwenden, doch diese haben das Problem, dass zumindest ersteres selbst von Hand schnell geknackt ist, und dass jeder z.B. die Länge des Passwortes selbst an der verschlüsselten Form sehen kann. Außerdem gibt es zu jedem unverschlüsselten Passwort genau ein verschlüsseltes Pendant.
Besser sind da Verschlüsselungsmechanismen, bei denen die verschlüsselte Form immer die selbe Länge hat, und deren verschlüsselte Form nicht eindeutig auf eine entschlüsselte Form zurückführen kann. Da dies unter Umständen schwer zu verstehen ist, lassen Sie mich das an einem Beispiel ausdrücken:
Gegeben ist das Passwort „JBB2011“. Der Verschlüsselungsalgorithmus schreibt nun vor: Das erste Zeichen in der verschlüsselten Form ist das letzte des Klarttextes. Das zweite Zeichen ist ein „~“, wenn der Klartext eine gerade Länge hat, ein „#“, wenn der Klartext eine ungerade Länge hat. Daraus ergibt sich die Verschlüsselte Form „1#“. Es ist unmöglich, den Klartext aus diesem verschlüsselten Text zu ermitteln, sodass das Passwort sicher bleibt.
Selbstverständlich sind die Verschlüsselungen, die in der Praxis verwendet werden, weitaus komplizierter.

Die bekanntesten Verfahren sind MD5 und SHA1. Diese kennen Sie.vermutlich als Checksumme bei Downloads. Beide sind aber inzwischen ziemlich einfach zu knacken, daher empfehlen wir, immer den neuesten, im Moment SHA-512 zu verwenden. Prüfen Sie daher vor dem Einsatz von Verschlüsselungsverfahren in Ihrem Programm, ob es nicht inzwischen eine noch sicherere Methode gibt. Unter Umständen kann es auch sein, dass Sie aufgrund einer älteren Java-Version nicht alle modernen Verfahren einsetzen können.

SHA-512 liefert immer einen verschlüsselten Hex-String mit genau 128 Zeichen zurück, egal, wie lang das Passwort war. Dieser String ist ist immer gleich. Der SHA-512-Wert eines leeren Strings ist immer:

cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e

Nutzen Sie diese Information zur Validierung ihrer SHA-512-Version.

Wie gehen wir jetzt damit vor? Nun ja, wir speichern das 128-stellige Passwort in der Datenbank und verschlüsseln das eingegebene Passwort des Benutzers. Dann vergleichen wir beide. Wenn Sie gleich sind, können wir davon ausgehen, dass das richtige Passwort gewählt wurde. Tatsächlich sind Kollisionen(=gleiche Verschlüsselung bei ungleichem Klartext) bei SHA-512 extrem selten, bereits eine geringe Veränderung des Klartextes erzeugt mit sehr großer Wahrscheinlichkeit einen komplett anderen Wert. Nun aber zum Code. Ich will hier nicht zeigen, wie Sie den verschlüsselten Text in oder aus der Datenbank bekommen, das können Sie im letzten Kapitel nachlesen.

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;

public class SHACreator{

  public static String getSHAValue(String text){
     try {
       MessageDigest digest = MessageDigest.getInstance( "SHA-512" );
       digest.update(text.getBytes());
       Formatter formatter = new Formatter();
       for(byte b: digest.digest()){
         formatter.format("%02x", b);
       }
       return formatter.toString();
     } catch (NoSuchAlgorithmException ex) {
       ex.printStackTrace();
     }
     return "";
  }
}

Die Klasse, die das eigentliche Verschlüsseln übernimmt, ist MessageDigest. Wir holen uns mittels der Methode getInstance(String algorithm) eine Instanz mit unserem gewünschten Algorithmus. Möglich sind hier unter anderem auch die SHA-Verfahren, „MD5“ oder „MD4“ (extrem unsicher). Mit der update(byte[] input) übergeben wir unsere Zeichenkette in Byteform. Wir können update(...) sooft aufrufen, wie wir wollen, der Hash-Wert wird entsprechend angepasst. Anschließend sagen wir dem MessageDigest, dass die Eingaben fertig sind und wir gerne den Wert hätten. Dies passiert mit digest() und gibt uns ebenfalls ein Byte-Array zurück, welches wir dann nur noch mit einem Formatter entsprechend formatieren und den resultierenden String zurück geben. Dies können wir dann nutzen, um Passwörter zu vergleichen.

SQL-Injektion

SQL-Injektion ist/war ein großes Problem, vor allem bei Internetanwendungen. Stellen Sie sich folgendes vor. Sie haben ein Suchfeld, welches einen Benutzer in der Datenbank sucht. Das entsprechende SQL-Statement sieht so aus:

connection.prepareStatement("SELECT * FROM user  WHERE name='"+inputString+"'");

Es wäre jetzt folgende Eingabe des Benutzers in die Suchmaske möglich:

peter' ;  DROP TABLE user; --

Zunächst wird die SELECT-Abfrage so zu Ende geführt, dass keine Fehlermeldung entsteht. Dann wird mittels „;“ ein neuer SQL-Befehl eingeleitet, der in diesem Fall die Benutzertabelle löscht. Das Doppelminus am Ende kommentiert alles, was sonst noch kommt, aus, sodass in Verbindung mit dem obigen SQL-Befehl dieser hier entsteht:

SELECT * FROM user  WHERE name='peter'; DROP TABLE user;

Und damit haben wir das Problem. Zugegeben, mit einer gelöschten Tabelle kommen Sie noch glimpflich davon. Beliebt ist aber auch, sich durch SET-Befehle Adminrechte zuzuweisen usw..

Doch jede Programmiersprache, die etwas auf sich hält, bietet inzwischen Methoden an, um das einzudämmen. So auch Java. Im letzten Kapitel haben wir von Statement und PreparedStatement gesprochen. Nun der Grund, warum Sie letzteres verwenden sollen, ist, dass dies bereits SQL-Injektion verhindern kann.
Schreiben Sie in die prepareStatement(...)-Methode das gesamte SQL-Statement und ersetzen Sie alle variablen Teile durch „?“:

PreparedStatement  ps = connection.prepareStatement("SELECT * FROM user  WHERE name=?");

In den folgenden Zeilen können Sie nun Schritt für Schritt die „?“ ersetzen:

ps.setString(1,inputString);

Der erste Parameter gibt den Index an, also welches Fragezeichen gemeint ist. Beachten Sie, dass dieser Index mit 1 beginnt.
Diese set-Methoden gibt es für alle Datentypen, auch für Objekte (siehe 09.08 Objekte speichern und laden (serialisieren/deserialisieren)) und diese Methoden sorgen dafür, dass SQL-relevante Eingaben so geändert(escaped) werden, dass Sie ungefährlich werden.

Wiederholungen vermeiden

Dieses Grundprinzip der Programmierung bleibt uns auch in der Datenbankprogrammierung nicht erspart. Stellen Sie sich vor, Sie haben ein großes Projekt mit sehr vielen SQL-Anweisungen. Und jetzt kommt Ihr Projektleiter und sagt, Sie müssten eine Tabelle wegen Überschneidungen mit einem anderen Projekt anders benennen. Viel Spaß beim Ersetzen jedes einzelnen SQL-Befehls.
Besser ist es, man zieht diese Möglichkeit von Anfang an in Betracht. Gewöhnen Sie sich an, Tabellen- und Spaltennamen irgendwo an einer Stelle im Programm zu definieren. Öffentliche, statische Konstanten bieten sich da gut an. Mit String.format(...) lässt sich dann gut das Statement zusammenbauen:

PreparedStatement ps = connection.prepareStatement(String.format("SELECT FROM %s WHERE %s=?",User.TABLENAME, User.NAME_COL));

Dies können Sie sogar noch weiter treiben, und auch den Rohstring „SELECT FROM %s WHERE %s=?“ auslagern, zum Beispiel in eine Bundle-Datei (siehe D) Mehrsprachigkeit mit Bundles in Java), sodass Sie das Programm selbst nach dem Kompilieren noch von außen kontrollieren und an Datenbanken anpassen können.

Übrigens: Über das PreparedStatement können keine Tabellen- oder Spaltennamen gesetzt werden, sondern nur feste Werte.

4 Replies to “17.03 Sicherheit bei der Arbeit mit Datenbanken”

  1. karl25

    SQL-Injektion:
    Man liest das immer wieder in dieser Form. Mir ist es aber bisher nicht gelungen, mit einem normalen Statement mehr als ein SQL-Statement abzusetzen. Offensichtlich interpretieren die meisten (alle?) Implementatoren von Treibern das „ein“ als „genau ein“ Statement.
    Kennt jemand ein jüngeres (>2005) Beispiel für SQL-Injektion via JDBC?

  2. Michael Kleiser

    Den Tipp mit den Konstanten für Tabellennamen und Spalten verstehe ich zwar. Aber kommt esl wirklich so häufig vor, dass die Tabellen und Spalten geändert werden?
    Der Nachteil den man sich damit einhandelt is, dass man so nicht mal eben schnell zur Fehlersuche das SQL-Statement in ein SQL-Ausführungs-Tools kopieren und ausprobieren kann.

  3. James Vornhagen

    Leider wird hier der Begriff „Verschlüsselung“ mit dem Begriff „Hash“ verwechselt.
    Ein Hash ist das Produkt einer mathematischen Berechnung, aufgrund dessen sich der Eingabe-Wert nicht zurückrechnen lässt.
    Das ist bei den oben genannten Verfahren der Fall.
    Bei einer Verschlüsselung ist es auf jeden Fall möglich auf den Ursprungswert zu kommen, dies ist entweder durch einen gemeinsamen Schlüssel(symmetrische Verschlüsselung) oder durch verschiedene Schlüssel public Key zum Verschlüsseln Private Key zum entschlüsseln (assymetrische Verschlüsselung) möglich.

    LG James

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.