Blog

Sichere Passwortspeicherung in Web-Applikationen

Moderne Web-Applikationen bieten häufig die Verwendung von Benutzerkonten an und müssen deshalb unvermeidlich Benutzerdaten speichern. Für die Authentifizierung gegenüber der Web-Applikation werden im Normalfall Benutzernamen und Passwörter verwendet.

Die Speicherung der Passwörter ist hierbei besonders prekär. Dieser Blogartikel behandelt das Thema sichere Passwortspeicherung und befasst sich dabei mit gängigen Fehlern sowie der korrekten Handhabung. Abschließend wird eine praktische Implementation in Python behandelt, sowie weiterführende Links für Sprachen wie Java, JavaScript, PHP und Ruby vorgezeigt. Der Artikel beschäftigt sich nicht mit dem Ziel der Speicherung auf dem Server, wie zum Beispiel eine Datenbank.

Problematik

Anbieter von Web-Applikationen sind dafür verantwortlich, dass Benutzerdaten vertrauenswürdig gespeichert werden. Bei leichtfertiger Handhabung leidet die Sicherheit der Benutzer und dementsprechend der Ruf des Anbieters.

Im schlimmsten Fall wird das Passwort auf dem Server in Klartext gespeichert, dies bedeutet, dass jeder mit Zugriff auf den Server unmittelbar Zugriff auf die Benutzerpasswörter hat, ob Dienstanbieter oder bösartiger Hacker.

Häufig werden Hashfunktionen verwendet, um die gespeicherten Passwörter unkennbar zu machen. Die Wahl der Hashfunktion ist essentiell, da viele weit verbreitete Methoden veraltet sind und somit kaum Sicherheit bieten.

Der Hashing Algorithmus MD5 ist immer noch häufig im Einsatz, obwohl bereits 1996 die ersten Schwachstellen in dem Algorithmus entdeckt wurden. Auch der oft verwendete Nachfolger SHA1 weist mittlerweile Sicherheitslücken auf. Als sichere Hashingfunktion kann SHA2, zukünftig SHA-3, angesehen werden.

Ein gängiger Angriff auf Hashsummen ist die Bruteforce-Attacke, bei der Hashsummen mit der eingesetzten Hashfunktion aus Unmengen aus verschiedenen Eingaben berechnet werden und mit dem Zielhash verglichen werden.

Ein Angreifer entwendet beispielsweise den MD5-Hashwert 5f4dcc3b5aa765d61d8327deb882cf99 von dem Server einer Web-Anwendung. Der Angreifer berechnet dann automatisiert die MD5-Hashwerte von verschiedensten Eingaben, bis einer der Hashwerte einer Eingabe mit dem entwendeten Hashwert übereinstimmt.

Eingabe:        Hashwert:
a               0cc175b9c0f1b6a831c399e269772661
aa              4124bc0a9335c27f086f24ba207a4912
aaa             47bce5c74f589f4867dbd57e9ca9f808
...             ...
password        5f4dcc3b5aa765d61d8327deb882cf99

Eine deutlich schnellere Identifizierung des Klartextes hinter dem Hashwert erlauben sogenannte Rainbow-Tables. Solche Tabellen enthalten unzählige vorberechnete Hashwerte von verschiedenen Eingaben oder weit verbreiteten Passwörtern. Ein Angreifer muss dann nur prüfen, ob der entwendete Hashwert in der Tabelle vorhanden ist und kann somit auf den Klartext zurückschließen.

Um solchen Angriffen entgegenzuwirken, werden häufig Hashfunktionen mehrfach angewendet, um eine direkte Identifikation zu vermeiden.

Beispielsweise wird der MD5-Hashwert der Eingabe berechnet, danach der MD5-Hashwert des ersten Hashwerts und so weiter. Gespeichert wird dann der zuletzt berechnete Hashwert.

Wiederholung 0:     MD5(password)                              = 5f4dcc3b5aa765d61d8327deb882cf99
Wiederholung 1:     MD5(5f4dcc3b5aa765d61d8327deb882cf99)       = 696d29e0940a4957748fe3fc9efd22a3
Wiederholung 2:     MD5(696d29e0940a4957748fe3fc9efd22a3)       = 5a22e6c339c96c9c0513a46e44c39683
...
Wiederholung 1000:  MD5(addbcea06efdd20f934b35e3b2111e55)       = d0fbc0823db90a58f25120f458350723

Die Anzahl der Wiederholungen wird oftmals daran orientiert, wie viele Wiederholungen der Server innerhalb einer vorgegebenen Zeit schafft. Beispiel: Innerhalb von 100 ms schafft der Server 500 Wiederholungen, deshalb wird die Anzahl der Wiederholung als 500 definiert. Mit schwachen Hashfunktionen bietet dies keinen angemessenen Schutz, sondern erhöht lediglich den Zeitaufwand für den Angreifer.

Sehr problematisch ist die Neuentwicklung von Methoden für die Passwortspeicherung. Die Entwicklung solcher Methoden birgt viele Gefahren und sollte keinesfalls eingesetzt werden.


Lösung

Sichere Passwörter sollten nicht limitiert werden, weder auf bestimmte Zeichen noch in deren Länge (mehr dazu später).

Um vorberechneten Rainbow-Tables entgegenzuwirken, wird oft ein sogenannter Salt eingesetzt. Dieser zufällige Wert ist für jeden Benutzer einer Web-Applikation einzigartig und wird zusammen mit den Passwort der Hashfunktion übergeben.

Mit dem zusätzlichen Salt wird das Vorberechnen von Rainbow-Tables extremst erschwert, weiters wird die Entropie der Benutzerpasswörter erhöht und das Identifizieren von identischen Passwörtern in der Datenbank verhindert.

Der zufällige Salt muss nicht zusätzlich verschlüsselt oder kodiert werden und wird in Verbindung mit dem Benutzer gespeichert. Die Länge des Salt sollte nach Möglichkeit >= 64 Byte sein.

Passwort = "password"
Salt = "t2qhpmrC1YH0dQvgOe1B7jJTTxMCQOxfQUTmGiwKnyiCikSwSuknHOMp7KrauyhQ"

Gespeichertes_Passwort = Hash(Passwort + Salt)

In Verbindung mit Salt wird oftmals auch von Pepper gesprochen. Pepper ist ein zufälliger Wert bestimmter Länge, der nicht gespeichert wird, und, mit Salt und Password addiert, der Hashfunktion übergeben wird. Bei der Überprüfung einer Passworteingabe generiert der Server jeden Hashwert für alle möglichen Pepper-Werte und prüft diese gegen den gespeicherten Hashwert.

Passwortspeicherung:

Passwort = "password"
Salt = "t2qhpmrC1YH0dQvgOe1B7jJTTxMCQOxfQUTmGiwKnyiCikSwSuknHOMp7KrauyhQ"
Pepper = 1 zufälliges Byte (z.B. 9)

Gespeichertes_Passwort = Hash(Passwort + Salt + Pepper)

Passwortprüfung:

Gespeicherter_Hash = "B109F3BBBC244EB824[...]7C95385FFAB0CACBC86"

Pepper_werte = [0,1,2,3,4,5,6,7,8,9]
Salt = "t2qhpmrC1YH0dQvgOe1B7jJTTxMCQOxfQUTmGiwKnyiCikSwSuknHOMp7KrauyhQ"

for Pepper in Pepper_werte:
    if Hash(Passwort + Salt + Pepper) == Gespeicherter_Hash:
        print("Pepper: "+ str(Pepper)+" OK!")
    else:
        print("Pepper: "+ str(Pepper)+" FEHLER!")

Die Ausgabe würde in diesem Fall folgendermaßen aussehen:

Pepper: 0   FEHLER
Pepper: 1   FEHLER
[...]
Pepper: 9   OK

Die zusätzliche Verwendung des Pepper-Werts erhöht die Sicherheit der gespeicherten Passwörter, da ein Angreifer im Besitz der Hash- und des Salt-Werte keine Möglichkeit hat, auf den Pepper-Wert zurückzuschließen. Der Angriffsaufwand wird somit weiter erhöht.

Der Nachteil an herkömmlichen Hashfunktionen ist, dass diese entwickelt wurden, um möglichst schnell Resultate zu liefern, was beispielsweise Bruteforce-Angriffen zugute kommt.

Spezielle Key-stretching-Funktionen wurden dafür entwickelt, Passwörter sicher zu repräsentieren und vor Angriffen zu schützen. Diese Funktionen sind im Vergleich zu herkömmlichen Hashfunktionen besonders langsam und robust gegenüber klassischen Angriffen auf Passworthashes.

Empfohlene Funktionen für die Speicherung von Passwörtern sind:

  • argon2
    argon2 ging als Sieger bei der Password Hashing Competition (2013-2015) hervor und sollte bevorzugt werden, sofern eine Implementation in der verwendeten Sprache existiert. Leider gibt es noch relativ wenige Implementationen des jungen Algorithmus.
  • scrypt
    Die scrypt-Funktion, ähnlich wie bcrypt und PBKDF2, wurde aber speziell dafür entwickelt, robust gegenüber Angriffen mit eigener Cracking-Hardware zu sein.
  • bcrypt
    bcrypt ist eine weit verbreitete und gut etablierte Passwort-Hashfunktion, die auf dem Blowfish cipher basiert. Es gibt zahlreiche Implementationen, aber die Passworteingabe ist auf 72 Byte limitiert.
  • PBKDF2
    Der Nachfolger von PBKDF1 ist ebenfalls eine Funktion zur sicheren Speicherung von Passwörtern, ist aber im Vergleich zu den oberen Funktionen deutlich anfälliger gegenüber Angriffen mit dedizierter Hardware (application-specific integrated circuit (ASIC)).

Diese Funktionen sehen die Verwendung von Salt und mehrfachen Iterationen bereits voraus und sind direkt implementiert.

Wegen der limitierten Verfügbarkeit von argon2-Implementationen wird vorerst die Verwendung von scrypt oder bcrypt empfohlen.

Eventuelle Probleme:

Die aufwändigere Berechnung der Passwort-Hashwerte kann zu Verfügbarkeitsproblemen führen, daraus kann möglicherweise ein Denial of Service (DoS)-Angriff resultieren. Um dies zu mitigieren, gibt es verschiedene Methoden. Eine zusätzliche Herausforderung für den Benutzer, wie zum Beispiel eine Captcha-Eingabe, kann das Problem limitieren. Verschiedene Web Frameworks, wie zum Beispiel Django, limitieren die Eingabe von Benutzerpasswörtern, um solche Angriffe zu verhindern.

Praktische Implementation in Python

Für die praktische Implementation wird davon ausgegangen, dass auf dem Server Python >= 3.6 zur Verfügung steht. Zur vereinfachten Verständlichkeit werden insgesamt vier Funktionen vorgestellt.

hash_password(user_id, cleartext): Eine Funktion, die eindeutige Kennung des Benutzers (z.B. Benutzername oder E-Mail-Adresse), sowie die Klartexteingabe des Benutzerpassworts erwartet. Die Funktion generiert dann den zu speichernden Hash.

save_hash(user_id, pw_hash): Diese Funktion erwartet wieder die eindeutige Benutzerkennung sowie den Hash, der von der ersten Funktion erstellt wurde. Beim Aufruf speichert die Funktion den Benutzer auf dem Server (z.B. in eine Datenbank).

check_password(user_id, cleartext): Mit dieser Funktion wird überprüft, ob ein Passwort für eine bestimmte Benutzer-ID übereinstimmt. Sie erwartet die Benutzerkennung sowie das eingegebene Klartextpasswort. Diese Funktion gibt, der Überprüfung entsprechend, Wahr oder Falsch zurück.

get_hash(user_id): Diese Funktion wird von der check_password()-Funktion aufgerufen und wird dafür verwendet, den gespeicherten Hash für einen bestimmten Benutzer auf dem Server aufzurufen (z.B. aus der Datenbank).

Bcrypt

Bcrypt kann mittels des Python Paket Managers pip installiert werden.

# pip3 install bcrypt
Collecting bcrypt
[...]
Successfully installed bcrypt-3.1.3 cffi-1.10.0 pycparser-2.17

Funktionen:

import bcrypt

hash_password(user_id, cleartext):
    # Das Klartextpasswort muss zuerst in Bytes, mit dem korrektem Encoding, umgewandelt werden.
    cleartext = bytes(cleartext, encoding='utf-8')

    # Soll die länge des Passworts nicht auf 72 Byte beschränkt werden, so wird das häufig mit SHA256 und base64 encoding umgangen.
    # Das eingegebene Passwort wird zuerst gehasht und danacht base64 encoded. z.B.
    # import haslib, base64
    # base64.b64encode(hashlib.sha256(cleartext).digest())

    # Der Salt wird direkt mit bcrypt erstellt und muss nicht extra gespeichert werden.
    # Dieser wird von bcrypt in dem Hash gespeichert.
    # Der Arbeitsaufwand zur Berechnung dafür kann gegebenfalls durch erhöhte Wiederholungen gesteigert werden (bcrypt.gensalt(100)). Der Standardwert für die Anzahl der Wiederholungen ist 12.
    hashed = bcrypt.hashpw(cleartext, bcrypt.gensalt())

    save_hash(user_id, hashed)

save_hash(user_id, hashed):
    # Diese Funktion speichert den generieten Hash und wird in diesem Blogartikel nicht behandelt
    pass

check_password(user_id, cleartext):

    hashed = get_hash(user_id)
    # Wie beim hashen/speichern muss das Passwort in Bytes umgewandelt werden.
    cleartext = bytes(cleartext, encoding='utf-8')
    # True oder False wird zurückgegeben
    return bcrypt.checkpw(cleartext, hashed)

get_hash(user_id)
    # Diese Funktion frägt den gespeicherten Hash für einen bestimmten Benutzer auf dem Server ab und gibt diesen an die aufrufende Funktion zurück. Dies wird in diesem Blogartikel nicht behandelt
    return hashed

Implementationen anderer Hashing-Methoden funktionieren meist gleich oder ähnlich. Das nächste Kapitel bietet Links für weitere gängige Sprachen.

Weitere Sprachen

Java

JavaScript

PHP

Ruby

Python


Fazit

Benutzerpasswörter müssen vor der Speicherung ausreichend geschützt werden. Hierfür kommen besondere Hashingfunktionen und Salt-Werte zum Einsatz. Der Salt ist für jeden Benutzer einzigartig und muss mit dem Hash gespeichert werden. Keinesfalls sollten Passwörter im Klartext gespeichert, schwache Hashfunktion verwendet oder eigene Lösungen erarbeitet werden.

Die korrekte Speicherung von Benutzerpasswörtern bietet zusätzlichen Schutz, sollten diese entwendet oder veröffentlicht werden.

Generell werden die Funktionen argon2, bcrypt, Scrypt oder PBKDF2 empfohlen. Diese sollte je nach Ansprüchen und Serverumgebung gewählt werden. Die Länge des Salt, die Anzahl der eventuellen Wiederholungen und weitere Parameter beeinflussen die Sicherheit der resultierenden Hashes sowie die Berechnungsgeschwindigkeit. Diese sollten je nach den Bedürfnissen der Web-Anwendung gewählt werden.

Vorheriger Beitrag
Willkommen im Securai-Blog
Nächster Beitrag
Serialisierungsformate und ihre Tücken

Ähnliche Beiträge

Es wurden keine Ergebnisse gefunden, die deinen Suchkriterien entsprechen.