PHP/Tutorials/Loginsystem

Aus SELFHTML-Wiki
< PHP‎ | Tutorials
Wechseln zu: Navigation, Suche

Der ursprüngliche Artikel Sessionbasiertes Loginsystem aus dem aktuell.de.selfhtml-Bereich befasste sich mit einem einfachen Loginsystem, bei dem man sich entgegen des Passwortschutz mittels .htaccess auch wieder abmelden kann.

Er schlug die Verwendung eines MD5-Hashes vor, was aber mittlerweile nicht mehr sicher angesehen wird. Dieser Artikel wurde im Laufe der Jahre immer wieder erweitert und gepflegt. Über die Versionsgeschichte des Wikis gelangen Sie weiterhin an die früheren Fassungen des Artikels.

Gerade angesichts der immer weiter um sich greifenden Fälle von Passwort-Diebstahl wie z. B. bei Yahoo[1] erschien es uns zu riskant, ein fertiges Loginsystems mit teils bekannten und teils unbekannten Sicherheitslücken anzubieten.[2] SelfHTML kann nicht sicherstellen, dass bestehende Implementationen des Loginsystems durch künftig erscheinende Verbesserungen aktualisiert würden. Im Folgenden werden grundsätzliche Sicherheitsaspekte erörtert.


Sicherheitsüberlegungen

Ein Login-System hat mit drei Sachen zu tun:

  1. übermittelte Nutzerdaten empfangen und verarbeiten
  2. Passwortvergleich mit "verschlüsselten" Nutzerpasswörtern
  3. Umgang mit Sessions
Hinweis:
Wenn (was regelmäßig der Fall ist) Passwörter nicht im Klartext gespeichert werden sollen, dann verwendet man keine Verschlüsselung sondern eine kryptologische Hash-Funktion. Das Ergebnis einer solchen Funktion nennt man Passwort-Hash, oder kurz Hash. Der Unterschied besteht darin, dass man ein verschlüsseltes Passwort wieder entschlüsseln kann, einen Hash aber nicht, da er mittels einer Einwegfunktion erzeugt wurde.


Fangen wir mit dem dritten Aspekt an:

Sessions

Mit dem HyperText-TransportProtokoll gibt es ein technisches Problem: Die Verbindung wird immer vom Client (in der Regel ein Browser) zum Server hin aufgebaut, eine Anfrage (der sogenannte Request) abgesetzt, die Antwort vom Server (die sogenannte Response) entgegen genommen und die Verbindung wieder getrennt. Wie soll da eine Anmeldung am Server technisch funktionieren, wenn die Verbindung nach dem Erhalt einer Serverantwort wieder getrennt wird?

Da HTTP ein sogenanntes zustandsloses Protokoll ist, braucht es eine Lösung, wie der Server einen Client wiedererkennen kann, wenn sich dieser bei einer neuen Verbindung mit einem neuen Request meldet. Das geschieht durch das Zuteilen einer Art Geheimnummer, einer eindeutig zuordbaren, zufällig generierten Folge von Ziffern und Buchstaben. Diese nennt man auch Session-ID. Anhand dieser ID kann der Server einen Client eindeutig wiedererkennen, weshalb ein Loginsystem in der Session zusätzlich lediglich eine Nutzerkennung speichern muss, um diese Session einem Nutzer in einer Datenbank ö. ä. zuordnen zu können. Es ist außerdem nicht nötig, irgendwelche Hashes von Passwörtern oder Ähnliches in der Session oder gar Cookies zu speichern, dies ist ein häufiges Missverständnis[3][4].

Die Session-ID

Das Übermitteln einer Session-ID kann prinzipiell auf zwei Arten erfolgen:

  1. als URL-Parameter
  2. als Cookie-Wert

Stellen wir uns vor, ein anderer Client „errät“ die Session-ID, dann kann er sich beim Server als derselbe Client ausgeben und von möglichen Privilegien profitieren, die der ursprüngliche Client eventuell gewährt bekommen hat. Daher ist die Session-ID etwas, das man möglichst nicht offen vor sich her trägt.

Eine Möglichkeit der Geheimhaltung ist also, den URL-Parameter nicht zu nutzen, da dieser in Server-Logs und auch in der Adresszeile des Browser erscheint. Man kann sogar Links oder Bookmarks generieren, die diese Identität enthalten, und diese Links dann an andere Nutzer weitergeben, damit diese vielleicht unberechtigt in den Genuss von Privilegien kommen können.

Wenn der Server bei jedem Aufruf die Session-ID ändert, dann kann der Client damit umgehen, indem er beim nächsten Request die neue Session-ID benutzt, andere Clients aber, die weiterhin die alte ID benutzen, haben beim nächsten Aufruf keine gültige ID mehr – oder der ursprüngliche Nutzer (in dem Fall sollte er schleunigst nach der undichten Stelle suchen und nach dem Abdichten sein Passwort ändern).

Wenn man URL-Parameter verstecken will, dann sollte man die zweite Lösung wählen und die Session-ID in einem Cookie speichern, was auch die empfohlene Lösung ist. Das setzt natürlich voraus, dass der Nutzer das Speichern von Cookies für diese Domain (im Prinzip diese Website) erlaubt. Aber allein das Nutzen eines Cookies anstatt eines URL-Parameters macht die Sache noch nicht sicher.

Grundsätzlich ist jedoch alles sehr unsicher, wenn die Verbindung zwischen Client und Server unverschlüsselt erfolgt, wenn also nicht HTTPS, sondern nur HTTP zum Einsatz kommt. Eine SSL- oder besser TSL-verschlüsselte Verbindung ist die Grundvoraussetzung für ein einigermaßen sicheres Loginsystem.

Das Manual von PHP widmet ein ganzes Kapitel zum Thema „Sessions und Sicherheit“: englische Version (garantiert aktuell), deutsche Version.

Sessions in PHP

In PHP gibt es einige Funktionen, um mit Sessions umzugehen. Im Wesentlichen verwendet man die Funktion session_start(), um eine Session überhaupt zu haben. Es gibt eine PHP-Einstellung, dass Sessions automatisch bei einem Request gestartet werden sollen (session.auto_start), jedoch kann diese PHP-Einstellung hinderlich sein, weshalb man sie besser deaktiviert lässt, was auch die empfohlene Voreinstellung ist. Hinderlich ist diese Einstellung dann, wenn man bei Besuchern nicht automatisch ein Cookie setzen möchte, nur weil diese die Website überhaupt aufgerufen haben, speziell dann nicht, wenn sie nicht auf der Login-Seite sind.

Der Session-Start bewirkt, dass eine vorhandene Session für den Client wiederaufgenommen wird. Dazu wird die im Request übergebene Session-ID ausgewertet. Kann keine Session-ID erkannt werden, erzeugt PHP eine neue und übermittelt sie in der Response zum Client – in der Standardkonfiguration als Cookie.

Ist eine Session gestartet worden, kann man das superglobale Array $_SESSION dazu benutzen, Daten für diesen Besucher darin zu speichern, z. B. ob sich der Besucher erfolgreich angemeldet hat.

Anmeldedaten via Formular

Der Nutzer soll Anmeldedaten an den Server übermitteln. Dazu braucht es ein geeignetes Formular, in welchem die Daten erhoben und versandt werden können. Da wir oben schon erkannt haben, dass sensible Daten nicht in URL-Parametern stehen sollten, ist dieses Formular zwingend mit der POST-Methode einzurichten:

Login-Formular mit ungeordneter Liste ansehen …
<form action="https://example.org/login.php" method="post">
  <ul>
    <li>
      <label for="login">Benutzer</label>
      <input id="login" name="login">
    </li>
    <li>
      <label for="pass">Passwort</label>
      <input id="pass" name="pass" type="password">
    </li>
    <li>
      <button>anmelden</button>
    </li>
  </ul>
</form>
Mit dem method-Attribut lässt sich die POST-Methode des Datenversandes einstellen. Als Zieladresse wurde explizit eine Seite mit einer verschlüsselten HTTP-Verbindung angegeben.

Auf der Serverseite müssen diese Daten nun empfangen und ausgewertet werden.

Passwort prüfen

Eine zentrale Angelegenheit bei einem Login ist das Prüfen eines Passworts. Eine Web-Anwendung will bei einem Anmeldeversuch ermitteln, ob die Kombination aus Benutzernamen und Passwort korrekt ist. Dazu braucht sie zumindest lesenden Zugriff auf Anmeldedaten. Diese können auf unterschiedlichste Art gespeichert sein, sei es in einer Datenbank, in einer speziellen Konfigurationsdatei oder auf sonst eine Weise.

Ein in der Fachpresse immer wiederkehrendes Thema sind Einbrüche in Systeme, bei denen dann Zugangsdaten erbeutet wurden. Besonders gravierend sind solche Vorfälle, wenn die Passwörter der Nutzer im Klartext oder gebrochenen Hash-Algorithmen wie MD5 gespeichert wurden, da dann die Kombinationen aus Benutzernamen und Passwort bekannt geworden sind. Daher wird allgemein empfohlen, die Passwörter nur als Hashes zu speichern, damit in einem Fall, in dem ein Angreifer die Zugangsdaten lesen kann, dieser die Passwörter trotzdem nicht erfährt, da sich ein Hash nicht umkehren und das Passwort nicht wieder in den Klartext zurückwandeln lässt. Nach einem Einbruch in die Datenbank – der nicht zwangsläufig sofort erkannt werden muss – sind die Passwörter erst einmal weiterhin sicher. Dennoch sollte man bei Bekanntwerden eines solchen Datenlecks die Nutzer zum Ändern ihrer Passwörter auffordern.


Password-API

PHP bietet seit der Version 5.5+ mit der Password-API mehrere Funktionen, die es PHP-Entwicklern erleichtern, sichere Passwortverwaltung zu implementieren.[5]

Wie man Passwörter richtig gehasht ablegt, muss man den Experten überlassen, die in PHP eine dafür eingerichtete Funktion bereitstellen: password_hash(). Warum man solcherlei Verfahren unbedingt Experten überlassen muss, liegt an der hohen Komplexität des Themas: (SELFHTML-Blog) Eine selbst gestrickte Verschlüsselung kann leicht teuer werden.
Außerdem bietet PHP hier über die Funktion password_needs_rehash() eine äußerst praktische und der Sicherheit förderliche Funktionalität: Welcher Hashalgorithmus mit welchen Parametern zum Hashen der Passwörter benutzt wird, bestimmt nicht die Anwendung, sondern die Konfiguration von PHP und diese kann im Laufe der Zeit und neuer PHP-Versionen an die aktuellen Erfordernisse angepasst werden, sodass die Anwendung nur durch Einsatz einer neuen PHP-Version und ihrer Konfiguration automatisch auf die aktuell als sicher bekannten Algorithmen benutzt.

Passwort richtig verschlüsseln
<?php

$encrypted_password = password_hash('Passwort im Klartext', PASSWORD_DEFAULT);

?>
Die Funktion password_hash nimmt als ersten Parameter das zu hashende Passwort, als zweiten Parameter eine Konstante für den Passwort-Algorithmus entgegen. Üblicherweise sollten Sie hier PASSWORD_DEFAULT verwenden, damit die Funktion die stärkste verfügbare Hashing-Funktion benutzt.

gehashte Passwörter prüfen

Man kann gehashte Passwörter nicht entschlüsseln. Das ist der Sinn, warum man Passwörter nur gehasht abspeichert. Aber wie will man dann testen, ob das von einem Benutzer eingegebene Passwort das richtige war?

Die Lösung ist folgende: Die Hashing-Funktion kann das vom Benutzer eingegebene Passwort ebenfalls hashen, um das Ergebnis mit dem gespeicherten Passwort-Hash zu vergleichen. Damit das Hashing des eingegebenen Passwortes mit dem gespeicherten Passwort-Hash vergleichbar wird, wird der gespeicherte Hash als eine Art Zutat in das Hashing des eingegebenen Passwortes gegeben. Ist das Ergebnis anschließend identisch, ist bewiesen, dass der Benutzer das richtige Passwort eingegeben hat.

Um diesen Vorgang dem Programmierer zu erleichtern, gibt es in PHP dafür die Funktion password_verify(), die zwei Parameter nimmt, nämlich das zu prüfende Passwort, und den gespeicherten Passwort-Hash:

Passwort auf Richtigkeit prüfen
<?php

$hashed_password = '$2y$07$BCryptRequires22Chrcte/VlQH0piJtjXl.0t1XkA8pw9dMXTpOq';

if ( password_verify($_POST['pass'], $hashed_password) ) {
  // Passwort war richtig.
  if( password_needs_rehash($hashed_password, PASSWORD_DEFAULT) ) {
    /*  Der Hashalgorithmus des gespeicherten Passworts genügt nicht mehr
     *  den aktuellen Anforderungen, daher sollte es mittels password_hash()
     *  neu gehasht und anstelle des alten Hashes in der Datenbank gespeichert
     *  werden; hier wird es nur in der entsprechenden Variable geändert:
     */
    $hashed_password = password_hash($_POST['pass'],  PASSWORD_DEFAULT);
    // ToDo: neu gehashtes Passwort in DB speichern!
  }
} else {
  // Passwort war falsch.
}

?>
In der Variablen $hashed_password steht ein gehashtes Passwort. Das gehashte Passwort ist hier nur für das Beispiel als Literal im Code angegeben. In einer „richtigen“ Anwendung wird man es aus einer Datenbank holen wollen. Im if-Statement wird die Funktion password_verify aufgerufen, der als erster Parameter das vom Client übertragene Passwort (siehe Formular oben mit pass als Name für das Passwort) übergeben wird, welches mit dem zweiten Parameter, dem gespeicherten Passwort-Hash, getestet wird. Anschließend wird noch mittels password_needs_rehash überprüft, ob der vorhandene Passwort-Hash mit den aktuellen Algorithmen gehasht wurde. Ist dies nicht der Fall, so wird das – aktuell im Klartext bekannte Passwort – neu gehasht und dieser Wert statt des alten in die Datenbank geschrieben.

Kein fertiges Loginsystem

Wir haben in diesem Tutorial gelernt, wo überall offensichtliche Stolpersteine hinsichtlich der Sicherheit eines Systems mit Login-Funktionalität liegen können. Wenn man dann noch in Betracht zieht, dass der Umgang mit vom Benutzer eingegebenen Daten ein prinzipielles Sicherheitsrisiko darstellt, dann muss man Laien dringend davon abraten, selbstgestrickte Lösungen zu basteln und einzusetzen, wenn sie dadurch wichtige Prozesse wie z. B. den Webshop einer Firma akut gefährden. Selbst erfahrene Programmierer können bei der Verarbeitung von Nutzereingaben Fehler machen, indem sie z. B. wichtige Kontextwechsel nicht beachten, oder an anderer Stelle ungeprüft Daten einfach übernehmen und so eine Sicherheitslücke schaffen.

Wie ein Formular mit PHP ausgewertet wird, wurde bereits im Artikel Formulare im PHP-Bereich exemplarisch vorgeführt, ein Login-Formular ist letztlich ein normales Formular. Ein vollständiges Login-Script möchten wir aber aus Sicherheitsbedenken nicht anbieten, sondern den Einsatz von erprobten Frameworks empfehlen, bei denen sich Experten ständig um neue Erkenntnisse und bekannt gewordene Sicherheitslücken kümmern, sodass Ihnen nur noch das ständige Aktualisieren der Framework-Software bleibt.

Quellen

  1. heise.de: Rekordhack bei Yahoo: Daten von halber Milliarde Konten kopiert vom 22.09.2016
  2. Self-Forum: Frage zum Wiki-Artikel ‚Loginsystem‘ Sven Rautenberg am 06.04.2016
  3. forum.selfhtml.org: Ein Loginscript
  4. forum.selfhtml.org: php sicherer Login
  5. forum.selfhtml: sicherer login

Weblinks