[Tutorial] Login Systeme von Einfach bis Profi

  • Vorwort


    Relativ häufig wird nach Möglichkeiten gefragt, wie man Uploads, einzelne Seiten oder ganze Bereiche einer Webseite mit einem Passwortschutz versehen kann. Dabei können die Anforderungen an so einen Passwortschutz ganz unterschiedlich ausfallen. In manchen Fällen reicht es aus, das nur ein Benutzer mit einem Passwort Zugriff erhält, während es in anderen Fällen nötig ist mehrere Benutzer, unabhängig voneinander, zu verwalten und Zugriff zu gestatten. Um verschiedene Anforderungen und Bereiche abzudecken, möchte ich Euch mit diesem Tutorial 3 Techniken etwas näher bringen, die Ihr als Basis für ein eigenes Login System verwenden könnt. Hauptbestandteil dieser Techniken ist das Session Management System von PHP. Aufgeteilt ist dieses Tutorial in:


    • Einfach - Ein simples Login System mit 1 Benutzer und 1 Passwort. Ausreichend um Uploads oder einzelne Seiten vor Zugriffe Unbefugter zu schützen. Kommt mit wenigen Zeilen Code und ohne Datenbank aus.
    • Fortgeschritten - Mit diesem System kann man beliebig viele Benutzer verwalten, da wir mit einer MySQL Datenbank arbeiten. Neben dem einfachen Login setzen wir auch Cookies ein, damit sich die Seite an den Benutzer erinnert.
    • Profi - Auch hier arbeiten wir mit einer MySQL Datenbank, wodurch beliebig viele Benutzer verwaltet werden können. Schwerpunkt bei diesem System liegt aber ganz klar auf dem Punkt Sicherheit. Es werden Schutzmaßnahmen gegen Session Fixation und Session Hijacking getroffen; der Benutzer wird bei jedem Seitenaufruf neu validiert; Hackversuche (Brute Force) werden erkannt und Benutzerkonten automatisch deaktiviert, bevor sie geknackt wurden; Passwörter werden durch die Zugabe von "Salts" sicherer gemacht; Benutzer werden bei längerer Inaktivität automatisch ausgeloggt, damit kein Unbefugter die Session übernehmen (hijacken) kann.




    Voraussetzungen


    • PHP 5.2.x oder höher
    • MySQL 5.x oder höher
    • PHP/MySQL Kenntnisse je nach Login System von Grundlagenwissen bis semi-professionell
    • ... und natürlich Lernbereitschaft


    Da der Quelltext weitestgehend kommentiert ist und entsprechende PHP Kenntnisse vorausgesetzt werden, werde ich die Erklärungen zu den Listings kürzer halten und nur wichtige Stellen etwas näher erläutern.
    Übrigens, für alle 3 Varianten lauten die Demo Zugangsdaten: Otto // geheim




    Variante Einfach


    Um "mal eben" eine Seite zu schützen oder für ein Datei Upload ein Passwort abzufragen, bietet sich diese Variante an. Die Zugangsdaten, sowie alle relevanten Funktionen für die Anmeldung sind direkt in die Login Datei geschrieben. Listing der Datei login_einfach.php:



    Die Funktionsweise ist einfach. Oben im Script legen wir Benutzername und Passwort fest, starten die Session, bereiten einige Variablen vor und sind für die Anmeldung gerüstet. Wird das HTML Formular abgeschickt, entfernen wir erst mal störende Maskierungen, die manchmal durch den Server hinzugefügt werden. Eine simple Kontrolle des eingegebenen Benutzername und Passwort mit den festgelegten Werten teilt uns mit, ob der Zugriff berechtigt ist oder nicht. Dazu konvertieren wir den Benutzername in Kleinbuchstaben, weil manche User faul sind und sich hartnäckig weigern die Shift-Taste zu benutzen. Beim Passwort wäre das natürlich fatal, da hier eine Mischung aus Groß-/Kleinbuchstaben sogar gewünscht ist. Erweisen sich die Zugangsdaten als korrekt, setzen wir die Session Variable $_SESSION['angemeldet'] auf den Wert true und wissen somit, dass sich der Benutzer korrekt angemeldet hat. Über die header() Anweisungen leiten wir den Benutzer auf die geheime (geschützte) Seite. Waren die eingegebenen Daten fehlerhaft, weisen wir der Variable $fehlermeldung die entsprechende Meldung zu, wodurch sich deren Existenz auf wahr ändert. Im HTML Teil fragen wir den Status dieser Variable ab und ist der Wert wahr, was er ja durch die Fehlermeldung ist, geben wir die Meldung an den Benutzer aus.


    Nachdem der Benutzer, nach korrekter Anmeldung, auf die geschützte Seite geleitet wurde, prüfen wir hier natürlich auch noch einmal, ob sich der Benutzer zurecht hier aufhält. Es könnte ja auch sein, dass er nur den Name der Datei weiß und die Seite direkt im Browser aufgerufen hat. Hier das Listing der Datei geheim_einfach.php:



    Uns interessiert nur der obere Bereich. Als erstes starten wir wieder die Session und prüfen, ob $_SESSION['angemeldet'] den Wert true hat. Ist das nicht der Fall, kam der Besucher nicht über die Login Seite und genau dorthin schicken wir ihn mit der header() Anweisung.


    [highlight]Wichtig:[/highlight] Wenn man in PHP mit Sessions arbeitet, muß man in jeder Datei das session_start() im oberen Teil einfügen. Unterlässt man es die Session zu starten, gehen die Daten aus der Session verloren!


    Das war's schon! Mit so wenigen Zeilen Code kann man ein Mini-Login System verwirklichen. Wie mehrfach erwähnt, eignet sich diese Variante aber lediglich um eher unwichtige Bereiche/Seiten zu schützen. Wer wirklich sensible Inhalte schützen möchte, sollte sich Variante Profi anschauen.


    Wer mehrere Benutzer hat, die individuelle Zugangsdaten haben sollen, für den bietet sich die folgende Variante an.




    Variante Fortgeschritten


    Diese Variante macht Gebrauch von einer MySQL Datenbank und speichert den Anmeldestatus in einem Cookie. Schauen wir uns zunächst einmal das Listing von login_fortgeschritten.php an:



    Der HTML Teil dieses Scripts ist identisch mit dem vorherigen Beispiel. Ebenfalls starten wir auch hier als erstes eine Session und bereiten einige Variablen vor, die wir im weiteren Verlauf benötigen. Danach folgt eine Funktion mit dem Namen db_connect(), die für uns die Kommunikation mit der DB übernimmt. Aus der Funktion wird das Resource Handle zurückgeliefert, mit dem wir weiterarbeiten können. Wer nicht versteht was in dieser Funktion vorsich geht, ist falsch in diesem Tutorial und sollte zunächst einmal einen PHP/MySQL Crashkurs durcharbeiten!


    Mit der Zeile

    PHP
    1. if (isset( $_COOKIE['UserLogin'] ))

    wird geprüft, ob ein Cookie existiert, das auf den Name UserLogin hört. $_COOKIE gehört, wie u.a. auch $_GET oder $_POST, ebenfalls zu den Superglobalen Arrays, die einen globalen Sichtbarkeitsbereich haben. Man kann also auch direkt aus einer Funktion darauf zugreifen. Der Name des Cookies stellt auch zugleich den Schlüssel des assoziativen Arrays dar. Wir können also mit $_COOKIE['UserLogin'] auf den Inhalt des Cookies zugreifen. Genau das machen wir auch im SQL Statement, indem wir einen Datensatz in der DB suchen, dessen Feld cookie_hash mit dem Wert im Cookie übereinstimmt. Finden wir einen Datensatz, handelt es sich offenbar um einen uns bekannten User. In diesem Fall setzen wir unsere Variable $_SESSION['angemeldet'] auf true und leiten den User auf unsere geheime Seite.


    Wurde kein Cookie gefunden, wird die Seite mit der Loginform angezeigt. Nach Abschicken des Formulars bereiten wir die Benutzereingaben auf, erzeugen einen md5-Hash vom eingegebenen Passwort und benutzen dieses, zusammen mit dem Benutzername, um unser SQL Statement zu formulieren. Wir lesen hier das Feld cookie_hash aus -das beim Anlegen des Benutzerkontos erzeugt werden muß und einmalig sein sollte! (ein md5-Hash der Registrierzeit würde sich anbieten)- das zum Datensatz gehört, bei dem die eingegebenen Zugangsdaten übereinstimmen. Haben wir einen Treffer, sind die Zugangsdaten korrekt und wir können den Benutzer einloggen.
    Wir setzen also die Variable $_SESSION['angemeldet'] wieder auf true, schicken einen Cookie zum Benutzer, damit wir ihn später wiedererkennen und leiten den Benutzer auf die geheime Seite weiter.


    Noch einmal zum setzen des Cookies. Der ganze Zauber findet in dieser Zeile statt:


    PHP
    1. setcookie( 'UserLogin', $usercookie['cookie_hash'], time()+600 );


    Mit setcookie() senden wir einen Cookie an den Browser des Benutzers. Ob ein Benutzer den Keks annehmen möchte oder nicht, liegt allein beim Benutzer. Es gibt keine Möglichkeit dem Benutzer einen Keks aufzuzwingen. Ebenso kann man nicht sofort prüfen ob der Keks beim Benutzer angekommen ist, sondern muß erst irgend eine Art von Reload auslösen. In unserem Fall geschieht das durch den header()-Redirect.
    Die Funktion setcookie() erwartet einige Parameter:

    • $name - Der Name des Cookie, über den wir den Inhalt auslesen können ($_COOKIE[$name])
    • $wert - Der Inhalt des Cookie. In der Regel Informationen die wir dort selbst abgelegt haben
    • $verfall - Ein Zeitstempel als INT Wert, bis wann der Keks gültig ist. (z.b. time()+60*60*24*30 wenn der Keks 30 Tage lang gültig sein soll)
    • $pfad - Damit kann man festlegen für welches Verzeichnis (mit Unterverzeichnisse) der Keks gültig sein soll. Wird kein Pfad angegeben, wird standardmäßig das Verzeichnis genommen in dem der Keks abgesetzt wurde.
    • $domain - Damit kann man den Keks allgemeingültig machen oder auf einzelne Sub-Domains beschränken
    • $sicher - Legt fest ob der Keks nur gültig ist, wenn er über eine HTTPS Leitung gesendet wurde. Der Wert von $sicher kann true oder false sein.
    • $nurHTTP - true oder false und legt fest, ob der Keks nur akzeptiert wird, wenn er über das HTTP Protokoll geschickt wurde. Es ist also nicht möglich den Keks durch Scripts o.ä. zu schicken. Dieser Wert sollte eigentlich immer auf true gesetzt werden, da er XSS Angriffe ungemein erschwert und somit wesentlich zur Sicherheit beiträgt. Hinweis: diese Option wurde erst mit PHP 5.2.0 eingeführt!


    Damit sollte das Login Script soweit klar sein. Kommen wir nun zur geheimen Seite; hier das Listing:



    Der HTML Teil ist wieder unspektakulär. Im Vergleich zu vorher ist hier nur ein Link eingefügt, mit dem sich ein Benutzer abmelden kann.
    Im oberen Teil wieder das übliche Spiel: Session starten!
    Anschließend prüfen wir ob die Variable $_SESSION['angemeldet'] gesetzt ist. Falls nicht, leiten wir den Benutzer zur Login Seite. Der aufmerksame Leser wird jetzt feststellen: "Aber wieso haben wir einen Keks gesetzt, wenn der User die Seite hier nicht direkt aufrufen kann?". Das ist korrekt, da der Wert in der Session noch nicht gesetzt wurde, wird der Benutzer zum Login umgeleitet. Auf der Login Seite wird aber als erstes geprüft ob ein Cookie existiert und leitet ggfs. wieder zur geheimen Seite weiter. Es ist also quasi nur eine doppelte Umleitung.


    Als letztes im Script wird noch geprüft ob die Variable $_GET['keks'] den Wert loeschen hat und falls dem so ist, löschen wir den Keks, zerstören die Session und leiten den Benutzer zur Login Seite weiter.
    Um ein Cookie zu löschen muß es von der gleichen Domain gesendet werden, den selben Namen haben und die Zeit bis zum Verfall muß in der Vergangenheit liegen. Dann, und nur dann, weiß der Browser, dass der Keks gelöscht werden soll.



    Bevor wir uns an die Variante Profi machen möchte ich noch erwähnen, dass die Anmeldung bei Login Scripts, sowie die Kommunikation zwischen Server und Client bei sensiblen Daten stets über eine sichere HTTPS Leitung geschehen sollte, da eine normale, unverschlüsselte Leitung nicht sicher ist. Speziell dieser Punkt ist bei einfachen Webpaketen nicht immer möglich, da extra eine SSL Lizenz für eine verschlüsselte Leitung gekauft werden müsste, die oftmals mehrere Hundert Euro pro Jahr kostet. Manche Provider bieten aber, bei höherwertigen Paketen, einen SSL Proxy an. Dann ist man zwar nur über eine allgemeine Domain (meist nach dem Schema [noparse]https://ssl.provider.de/meine-domain/[/noparse]) erreichbar, aber dafür ist es wenigstens sicher!

  • Variante Profi


    Die Variante Profi legt Wert auf einen möglichst hohen Sicherheitgrad. Manche der hier gezeigten Dinge sind auf Mietwebspace meistens, falls überhaupt, nur bei sehr guten Providern möglich, da sie ein Anpassen der php.ini erfordern. Schauen wir uns zunächst einmal das Listing der Datei login_profi.php an. Danach erläutere ich die einzelnen Schritte und binde eigene Funktionen mit in die Erklärung ein. Sämtliche Funktionen wurden in die Datei funktionen.inc.php ausgelagert. Das komplette Script gibt es am Ende des Tutorials noch einmal als Download zum selber testen. Inhalt der Datei login_profi.php:



    Als erstes setzen wir das Error Reporting auf 0, damit sämtliche Fehlermeldungen unterdrückt werden. So gut Fehlermeldungen während der Entwicklung sind, so schädlich können sie im live Einsatz sein. Fehlermeldungen verraten einem Angreifer sehr viel über die Struktur einer Datei, Datenbank, Funktionsweise des Scripts und erleichtern somit seine Arbeit.
    Die nächsten beiden Zeilen betreffen die php.ini. Damit wird erzwungen, dass die Session Kennung via Session Cookie transportiert wird. Auf diese Weise kann man keine Referrer mit sichtbarer Kennung in fremden Logfiles hinterlassen, was zu gestohlenen Sessions (Session Hijacking) führen kann. Ob und in welchem Umfang man Zugriff auf die php.ini hat, ist von Provider zu Provider unterschiedlich. Manche erlauben ein verändern der Werte via ini_set() oder via .htaccess, während wieder andere Provider eigene php.ini Dateien direkt im Verzeichnis erlauben oder man kann über die Accountverwaltung Werte anpassen. Die Frage ob und wie das bei Euch möglich ist kann nur euer Provider beantworten!
    Dann folgt das obligatorische starten der Session. Da ein Benutzer mit einer bereits aktiven Session auf unsere Seite kommen kann und unser session_start() in diesem Fall lediglich die Session fortführen würde, müssen wir sicherstellen, dass die Session auch tatsächlich zuvor von unserem Server initiiert wurde und nicht aus fremder Quelle übergeben wurde. Dieses "aus fremder Quelle" übergeben einer Session Kennung an ein potentielles Opfer nennt sich Session Fixation. Ein Angreifer kann eine Session nur dann übernehmen/stehlen, wenn die Session Kennung bekannt ist. Da der Angreifer diese Kennung selbst erzeugt haben kann und an sein Opfer weitergegeben hat, kennt er die Kennung natürlich! Um das zu verhindern stellen wir sicher, dass nur eine von uns selbst vergebene Kennung Gültigkeit hat.
    Dazu prüfen wir, ob in der Session die Variable $_SESSION['server_SID'] den Wert true hat. Trifft das nicht zu, resetten wir die Session, wandeln die Session in ein leeres Array um, zerstören die Session, starten die Session komplett neu, generieren eine neue Kennung (dem session_regenerate_id() fällt eine besonders wichtige Rolle zu, auf die ich weiter unten noch detaillierter eingehen werde) und setzen letztendlich den Wert der Variable $_SESSION['server_SID'] auf true.
    Durch das starten, vollständige zerstören, neu starten der Session stellen wir sicher, dass unser Script gegen Session Fixation abgesichert ist!
    Jetzt binden wir die benötigten Funktionen ein, bereiten einige Variablen vor und öffnen eine Datenbankverbindung, deren Handle wir in $conid ablegen.


    Wir prüfen ob das Forumlar abgeschickt wurde und falls ja, bereinigen wir erst einmal die Benutzereingabe.

    PHP
    1. $eingabe = cleanInput();

    Die dazugehörige Funktion sieht so aus:



    Große Erklärungen sollten nicht nötig sein. Die Eingabe wird aufbereitet und ein Array mit dem Benutzername und Passwort zurückgeliefert, wonach im öffentlichen Bereich des Scripts fortan das Array $eingabe zur Verfügung steht.


    Nun wird geprüft, ob ein entsprechender Benutzer in unserer Datenbank existiert.

    PHP
    1. $anmeldung = loginUser( $eingabe['benutzername'], $eingabe['passwort'], $conid );

    Die Funktion loginUser() erwartet 3 Parameter: 1) Benutzername 2) Passwort 3) Verbindungskennung. Die Rückgabewert wird in $anmeldung abgelegt und kann entweder true (Benutzer gefunden) oder false (Login fehlerhaft) sein.
    Diese Funktion loginUser() ist der größte Brocken im ganzen Script, also werfen wir mal einen Blick auf das, was in der Funktion passiert.



    Als erstes stellen wir eine Anweisung zusammen mit der wir das Feld passwort_zusatz auslesen, das zum Datensatz des Benutzername gehört und welches aktiviert=1 hat. Der Benutzername muß in der DB vom Typ Unique sein, damit es hier keine Überschneidungen gibt. Dieser Passwort-Zusatz wird auch Salt (eng. Salz) genannt und dient dazu Passwörter sicherer zu machen. Dieses Salt muß für jeden Benutzer einmalig zufällig erstellt werden (beim anlegen des Benutzer) und wird in der Datenbank abgelegt. Wurde von der DB ein Treffer zurückgeliefert wissen wir, das der Benutzername in unserem System existiert. Aus der Kombination eingegebenes Passwort + Salt aus der DB erzeugen wir einen md5-Hash, der unser tatsächliches Anmeldepasswort darstellt.


    Klären wir noch schnell die Frage "Was soll das mit dem Salt?".
    Wenn mehrere Benutzer das selbe Passwort haben, so ist der md5-Hash immer identisch. Kennt man also ein Passwort mit zugehörigem Hash und sieht irgendwo diesen Hash wieder weiß man, dass der andere Benutzer das selbe Passwort verwendet. Der Angreifer hat also keinerlei Arbeit sich Zugang zu einem Account zu verschaffen. Um es den Angreifern noch einfacher zu machen gibt es sogenannte Rainbow Tables, in denen etliche Millionen Hashes gespeichert sind, mit denen man binnen Sekunden die echte Identität eines Passwort ermitteln kann. Weil wir aber einen zufälligen Salt mit in's Passwort mischen, ist der Hash jedesmal anders, selbst wenn das reine Passwort identisch ist.
    Lange Rede kurzer Sinn: Passwörter anhand ihres Hash zu erraten oder mithilfe von Rainbow Tables zurückzuwandeln ist unmöglich, wenn man Passwörter mit einem geheimen Salt versieht und den damit erzeugten Hash in der DB ablegt. Passwörter sollten niemals im Klartext gespeichert werden!


    Zurück zum Script ...
    Wir haben jetzt also das Salt ausgelesen und unser Anmeldepasswort erzeugt. Wir schicken jetzt eine neue Anweisung an die Datenbank und lesen das Feld fehlversuche aus, das zum Datensatz gehört auf den der Benutzername + Anmeldepasswort passt. Haben wir einen Treffer, stimmen die Anmeldedaten offensichtlich überein und es handelt sich hier um unseren User. Wir resetten noch kurz den Wert von fehlversuche, falls dieser nicht 0 ist und melden mit return true; die erfolgreiche Anmeldung an's Hauptscript.
    Haben wir bei der Abfrage mit dem Benutzername + Passwort keinen Treffer erzielt wissen wir, dass zwar der Benutzername stimmt, nicht aber das Passwort! Da wir auf Sicherheit setzen, gehen wir also vom Schlimmsten aus und denken erst mal, dass es sich um einen Angriff handelt. Deswegen aktualisieren wir im else-Zweig das Feld fehlversuche und erhöhen den Counter um 1. Anschließend lesen wir das Feld fehlversuche erneut aus um zu sehen, ob das Limit Fehlversuche (in unserem Fall 10) erreicht wurde. Da sich vermutlich kein Mensch 10 mal beim eigenen Passwort verschreibt, hat hier jemand versucht mit Brute Force das Passwort zu knacken. Um dem einen Riegel vorzuschieben, deaktivieren wir das Benutzerkonto, indem wir aktiviert auf 0 setzen. Jetzt kann unser Angreifer so lange probieren wie er möchte, er wird sich nicht einloggen können, selbst wenn das Passwort korrekt wäre. Wer aufgepasst hat wird gesehen haben, dass wir bei jeder DB Abfrage immer den aktiviert Status mit einbezogen haben. Ist das Konto deaktiviert, kann man gar nichts machen, bis das Konto vom Admin wieder aktiviert wurde.



    Wir haben nun also geklärt ob die eingegebenen Benutzerdaten korrekt waren oder nicht, oder ob jemand versucht hat das Konto zu hacken. Demnach haben wir im Hauptscript in $anmeldung jetzt ein true oder false stehen. Im Falle von true geht's weiter mit

    PHP
    1. $update = updateUser( $eingabe['benutzername'], $conid );

    In der Funktion updateUser() findet die eigentliche Anmeldung statt. Zusätzlich speichern wir noch verschiedene Daten, anhand deren wir den Benutzer während des Aufenthalts auf unserer Seite identifizieren.



    Wie wir sehen aktualisieren wir den Benutzerdatensatz und speichern neben der IP Adresse auch die Identifikationsmerkmale des Browsers (Type, OS, Build Version, usw.). Da die IP Adresse generell eher ungeeignet ist als Identifikationsmerkmal, und auch die Browserkennung nicht einmalig ist, speichern wir auch den md5-Hash der Anmeldezeit. Dieser Wert dürfte, in Verbindung mit dem Benutzername, ziemlich zuverlässig sein. Wurde der Datensatz erfolgreich aktualisiert, findet erst jetzt die eigentliche Anmeldung statt!
    Nun setzen wir die Session Variablen $_SESSION['angemeldet'] auf true, sowie den Benutzername und die Anmeldezeit und liefern ein true in's Hauptscript zurück. Anschließend leiten wir den Benutzer mit einer header()-Anweisung zur geheimen Seite weiter.


    Wichtig: Wenn Fehler auftreten, sei es falsches Passwort oder der Benutzername ist unbekannt, teilen wir lediglich mit das bei der Anmeldung ein Fehler aufgetreten ist, nicht aber welcher! Würden ein Angreifer versuchen den Account mit dem Name Peterchen zu hacken und das System meldet "Benutzer unbekannt", bräuchte der Angreifer gar nicht weiter versuchen. Wieso sollte er auch einen Account hacken, der gar nicht existiert?! Versucht er nun sein Glück mit dem Account Fritzchen und auf einmal taucht die Meldung "Passwort ist falsch" auf, wo worher immer "Benutzer unbekannt" stand, weiß der Angreifer, dass dieses Konto tatsächlich existiert und kann sich an's knacken des Passworts machen.



    Okay, der Benutzer ist nun auf der geheimen Seite geheim_profi.php:



    Der obere PHP Teil, der so auf jeder (!) geschützten Seite stehen muß, ist bereits vertraut und bedarf (hoffentlich) keiner Worte!
    Hier kontrollieren wir mithilfe von

    PHP
    1. if (!checkUser( $conid ))

    ob der Benutzer sich auf dieser Seite aufhalten darf. Die Funktion checkUser() hat folgenden Inhalt:



    Das erste das in der Funktion geschieht ist

    PHP
    1. session_regenerate_id( true );


    Diese Zeile ist ungemein wichtig! Diese PHP Funktion sorgt dafür, dass eine neue Session Kennung initiiert wird und der Inhalt der alten Session in die neue übernommen wird. Der Parameter true existiert erst seit PHP 5.2.x und sorgt dafür, dass nicht nur eine neue Kennung generiert wird, sondern es werden auch sämtliche alten Reste der vorherigen Session gelöscht. Das erhöht die Sicherheit von Sessions deutlich, da es nicht mehr möglich ist, dass sich Unbefugte über einen Shell-Zugriff die alte Session Datei auf dem Server aneignen und auslesen und so u.U. an geheime Informationen gelangen.


    Als nächstes wird geprüft, ob der Benutzr mit $_SESSION['angemeldet'] angemeldet ist. Nun lesen wir die gespeicherten Vergleichsmerkmale aus der DB aus und vergleichen jeden dieser Punkte mit dem aktuell angemeldeten Benutzer. Stimmt ein Wert nicht überein, liefern wir false zurück. In der Zeile

    PHP
    1. if (($benutzerdaten['zuletzt_aktiv'] + 600) <= $_SERVER['REQUEST_TIME']) return false;

    prüfen wir zusätzlich, wie lange die letzte Aktivität auf der Seite zurückliegt. Wurde für 10 Minuten keine Aktivität registriert, wird der Benutzer zwangsabgemeldet. Das macht man deswegen, weil es Benutzer gibt die sich anmelden und dann auf anderen Seiten surfen oder den Rechner verlassen, während der Browser geöffnet bleibt. Das Problem dabei ist, dass die Session erst erlischt, wenn der Browser geschlossen wird. Mit anderen Worten: der Benutzer bleibt so lange angemeldet, bis er den Browser schließt, selbst wenn er schon lange nicht mehr auf der Seite war. Es könnte also jemand die Session übernehmen, während die Session aktiv ist. Durch das 10 Minuten Limit begrenzen wir das Zeitfenster in dem Schaden entstehen könnte.


    Wurde die Funktion bisher nicht durch ein return false; verlassen, handelt es sich um den echten Benutzer und wir aktualisieren den Datensatz dahingehend, dass wir die Zeit der letzten Aktivität festhalten.


    Kommen wir zur letzten Funktion. Wurde ein false zurückgeliefert oder der Benutzer klickt den "Abmelden"-Link, wird die Funktion resetUser() aufgerufen:


    PHP
    1. function resetUser()
    2. {
    3. session_destroy();
    4. header( 'location: login_profi.php' );
    5. exit;
    6. }


    Hier geschieht nichts spektakuläres. Es wird lediglich der Inhalt der Session gelöscht und auf die Login Seite umgeleitet. Auf der Login Seite angekommen treten wieder unsere Anmelde- und Schutzmechanismen in Aktion.


    [highlight]Hinweis:[/highlight]
    Noch ein Wort zu den Erkennungsmerkmalen eines Benutzers. Während die Browserkennung als zusätzliches Merkmal durchaus brauchbar ist, auch wenn viele den selben Browser und OS verwenden, gibt es doch erstaunlich häufig Abweichungen in SP Nummern, Build Versionen, etc., so ist die IP in den meisten Fällen ungeeignet!
    Zum einen kann es sein das Benutzer durch Proxies surfen und sich somit ständig die IP ändert (z.B. AOL User benutzen zwangsweise Proxies, weil AOL es seinen Benutzern diktiert!), zum anderen sind speziell in Firmen meistens NAT Netzwerke installiert, wodurch sich eine handvoll bis mehrere Hundert oder Tausend Benutzer eine identische IP teilen. Man sollte sich also überlegen, ob man die IP zur Identifikation benutzen möchte!
    Auf jeden Fall sollte aber noch etwas einmaliges zur Identifizierung genommen werden. In unserem Beispiel ist das der Benutzername in Kombination mit dem Hash der Anmeldezeit.




    Fazit


    Damit kommen wir zum Ende dieses Tutorials. Es wurde mal wieder länger als geplant, aber beim Thema Sicherheit sollte man das verzeihen können. :)
    Bleibt zu hoffen, dass der ein oder andere etwas damit anfangen kann und die eigene Seite damit etwas sicherer wird. Um Missverständnisse auszuschließen hier noch der Hinweis:
    Dieses Tutorial soll als Grundlage für eigene Login System dienen und ist nicht dazu gedacht, dass es genau so 1:1 übernommen wird! Ebenso geschieht die Benutzung auf eigene Gefahr und weder das Traum-Projekt noch der Autor dieses Tutorials können für Schäden, die durch das Script entstanden sein könnten, haftbar gemacht werden!


    Zum Abschluß noch einige Links zu den verwendeten Funktionen und weiterführende Informationen zum Thema Sessions und Sicherheit.



    Linkübersicht


    Sessions und Sicherheit

    • [wiki=Session Fixation]Session Fixation[/wiki] (deutsch)
    • [wiki=Session Hijacking]Session Hijacking[/wiki] (deutsch)
    • Session Fixation (englisch)
    • [wiki=Rainbow Table]Rainbow Table[/wiki]



    PHP Funktionen

    • [phpfunction]error_reporting[/phpfunction]
    • [phpfunction]exit[/phpfunction]
    • [phpfunction]get_magic_quotes_gpc[/phpfunction]
    • [phpfunction]header[/phpfunction]
    • [phpfunction]ini_set[/phpfunction]
    • [phpfunction]is_resource[/phpfunction]
    • [phpfunction]isset[/phpfunction]
    • [phpfunction]md5[/phpfunction]
    • [phpfunction]mysql_free_result[/phpfunction]
    • [phpfunction]mysql_real_escape_string[/phpfunction]
    • [phpfunction]session_destroy[/phpfunction]
    • [phpfunction]session_regenerate_id[/phpfunction]
    • [phpfunction]session_start[/phpfunction]
    • [phpfunction]session_unset[/phpfunction]
    • [phpfunction]stripslashes[/phpfunction]
    • [phpfunction]strtolower[/phpfunction]
    • [phpfunction]trim[/phpfunction]
  • Respekt, da hat sich der "Buddy" mal wieder was richtig brauchbares einfallen lassen. Und wie immer auch gut ge- und beschrieben. Danke Dir Rizzo hierfür :):)

    Schöne Grüße aus Thüringen
    Stephan Page


    Stell Dir vor, hier steht was und keiner liest es!! schon entdeckt?? F1 ist ne geile Taste
    [FONT="Comic Sans MS"]Ich beantworte keine E-Mails. Bitte alle Fragen ins Forum


    [COLOR="RoyalBlue"][FONT="Comic Sans MS"]schon gehört??? Das Internet ist voll, die lassen keinen mehr rein!! :p :D :p

  • Moin,


    Login System - Prima Sache. Kann man immer gebrauchen.


    Kann mich nur anschliessen, super beschrieben, alles dabei.
    Auch an die faulen Tipper gedacht :D


    Danke für Ergebnis und die Mühen, werds mal probieren...

  • Danke für die Blumen. :) Aber gibt nichts zu danken, weil das ja jetzt nichts sooo großes ist. Der dickste Brocken liegt mit Teil 2 aber noch vor mir, weil das Listing der funktionen.inc.php schon fast länger ist als der gesamte 1. Teil dieses Tutorials. :rolleyes: :D



    p.s.: Jaaa ich weiß Mark, ich hab's ja versucht kurz zu halten, aber ich kann's eben nicht besser. :p

  • [Blockierte Grafik: http://images.buto.eu/bitte.gif]


    ;)



    [edit]Passend zum 3. Beispiel ganz aktuell die Meldung auf heise Security: Apples Webbrowser Safari anfällig für Session-Fixation-Angriffe
    Im Artikel heißt es unter anderem:

    Zitat

    Allerdings hängt der Erfolg dieser auch Session Fixation genannten Attacke von der konkrekten Implementierung der Web-Anwendung ab, beispielsweise ob noch die IP-Adresse oder andere Informationen in die Sitzungsdaten eingehen.

    [/edit]

  • Echt klasse Tutorial! (Wie die anderen von dir übrigens auch, hab hier die letzten Tage mal ein bisschen rumgestöbert)


    Eine Frage hätte ich zur Profivariante...


    $_SESSION['server_SID']


    Ich versteh nicht inwiefern das die Session Fixation unterbindet... der Angreifer kann doch in seiner eigenen Session diesen Wert auch speichern... er muss das Script dazu natürlich kennen... aber mit ein paar Tricks sollte es einem guten Angreifer doch möglich sein mal zu gucken, was bei einem legal angemeldeten User in der Session drin steht (wenn er z.B. selbst Mitglied ist), oder?


    Hoffe meine Frage ist verständlich ;)


    mfg
    Martin

  • Hallo Martin,


    Wenn man nur schaut ob die Session existiert, ist noch lange nicht gewährleistet das sie auch vom Server initialisiert wurde. Wenn Du eine Seite z.B. mit index.php?PHPSESSIONID=Hamster aufrufst, legt der Server u.U. eine Session mit dem Name Hamster an. Schickt man einem Opfer einen Link mit diesem Anhang, übernimmt der die von Dir gestartete Session.
    Dadurch das wir die Session zuerst killen und anschließend eine neue vom Server erzeugen, verhindert man, dass ein User bereits mit einer aktiven Session auf die Seite kommt. Schau doch mal im Anhang in die Links, dort wird das ganz gut erklärt.
    So ohne weitere kann ein Angreifer auch nicht mal eben in die Session schauen, weil diese Informationen auf dem Webserver abgelegt sind und nicht beim Benutzer auf dem Rechner. Auch das klauen der Session ID ist so nicht möglich, da bei jedem Aufruf eine neue generiert wird, die zu diesem Zeitpunkt ebenfalls nur dem Server bekannt ist. Selbst wenn jemand die ID über die Adresse auslesen könnte, was wir aber auch unterbunden haben, würde er immer nur die vorherige ID bekommen, die zu diesem Zeitpunkt schon nicht mehr aktuell ist.

  • Hm... ok... ich glaube die Probleme, die ich damals mit meinem Script hatte, werden bei dir an anderer Stelle behoben...


    ich hatte folgendes Problem... ich habe zur Erkennung nur überprüft, ob eine User-ID in der Session liegt (die wurde da halt reingesetzt, wenn man sich einloggt)... da ich dieses Login-Script aber auf zwei Seiten verwendet hatte, konnte man sich auf der einen einloggen und dann auf die andere gehen... schwups war man dort bei dem User eingeloggt, der dieselbe ID hatte, wie man selbst auf ersterer Seite...


    Und da hab ich mir halt nur grad überlegt, dass es das auch nicht sicherer gemacht hätte, wenn man zusätzlich noch auf "server_SID" prüft, da das andere Script diesen Wert ja auch setzen würde... darauf wollte ich eigtl. hinaus.


    Doch bei deinem Script wird es wohl eher schwierig über so einen Trick in einen Account zu kommen, da z.B. der Hashwert der Anmeldezeit kaum übereinstimmen wird.


    Hoffe, ich hab jetzt nicht noch mehr durcheinandergewürfelt. Ist schon arg spät ^^.


    mfg
    Martin

  • Hallo Martin,


    genau dieses Szenario wird damit ja unterbunden. Da wir nur Session Kennungen zulassen die vom Server vergeben wurden, wird ausgeschlossen, dass ein anderer User die Session übernehmen kann. Dieses übernehmen der Session ist das oben erwähnte Session Hijacking.
    Die Session Kennung vom Server wird zufällig generiert und die Wahrscheinlichkeit das dieser Zufallswert bei 2 Usern gleich sein kann ist 0%. Eine Session ist eine Datei die auf dem Server abgelegt wird. Es können keine 2 Dateien mit dem gleichen Name, aber unterschiedlichem Inhalt existieren.

  • Ok, ich glaub ich habs verstanden... war ein technisches Problem. Ich hatte immer so die Vorstellung, dass die Session-Daten auch in einem Cookie hängen und nicht aufm Server. Dann ist auch klar, warum die Überprüfung eines Booleans ausreicht. Vielen Dank.


    eins noch: wenn ich dieses Log-In Script in zwei User-Bereichen auf demselben Server verwenden würde, wäre es nicht mehr ausreichend oder? Dann müsste man noch überprüfen ob die Session im Rahmen des entsprechenden Bereiches vergeben wurde, oder?


    mfg
    Martin

  • Hallo Rizzo!


    Was für ein Tutorial. Genau danach habe ich gesucht. Du weisst wie der Hase läuft.:D Habe aber noch eine Frage: Du hast meine Interesse mit dem Passwort-Zusatz (Salt) geweckt. Aber du hast angesprochen das dies schon bei der Anmeldung generiert werden muss. Aber wie?

  • Hallo Iron Man,


    was Du als Salt benutzt spielt keine Rolle. Das kann ein Fragment von einem MD5 Hash sein, den Du aus einem Zeitstempel erzeugst, oder eine willkürliche Zahlen-/Buchstabenfolge. Wichtig ist nur, dass es einmalig in der DB vorkommt und nicht einfach so zu erraten ist.

  • Super Tutorial!
    Nachdem im Profi-Teil auf Cookies verzichtet wird, kann man sagen, dass Cookies unsicher sind?
    Hätte man 1x den Cookiewert von der "Fortgeschrittenen"-Variante heraußen würde man sich immer mit einem selbst erstellen Cookie ohne je User oder Passwort gesehen zu haben einloggen können, richtig?
    Den Wert kann man aber nicht schützen. Ein Verändern des Wertes verhindert dann den cookielogin, daher unsicher, richtig?


    Wenn ich beim FF einstelle, keine Cookies annehmen, funktionieren bei mir auch keine Sessions. Stimmt es, dass das dann mit dem php.ini Wert
    session.use_trans_sid zusammenhängt, da dieser (so wie im Tutorial) das Schreiben der SessionId in die Adressleiste verhindert?

  • Hallo Peter!


    Super Tutorial!


    Danke :)



    Nachdem im Profi-Teil auf Cookies verzichtet wird, kann man sagen, dass Cookies unsicher sind?


    Es wurde deswegen auf Cookies verzichtet, weil die Profi-Variante zum Schutz von wirklich sensiblen Daten gedacht ist. Würde man ein Auto-Login per Cookie benutzen, könnte jeder Benutzer, der nach dem eigentliche Berechtigten am Rechner sitzt (z.B. im Büro, im Inet-Cafe, Familien PC, usw.), einfach auf die geschützen Daten zugreifen. Durch das Erzwingen des manuellen Login bei jedem Besuch, wird dieser "stehlen" des Zugangs verhindert.



    Wenn ich beim FF einstelle, keine Cookies annehmen, funktionieren bei mir auch keine Sessions. Stimmt es, dass das dann mit dem php.ini Wert
    session.use_trans_sid zusammenhängt, da dieser (so wie im Tutorial) das Schreiben der SessionId in die Adressleiste verhindert?


    Session-Cookies sollte man immer akzeptieren, da es eben sonst zu Einschränkungen auf Seiten kommen kann, die diese voraussetzen. Schaltet man Cookies komplett ab, hat man eben Pech gehabt.
    Das Transportieren der Session-Kennung per URL ist bei sensiblen Loginbereichen ebenso schlecht wie Cookies, da man so u.U. Unbefugten Zutritt verschafft. Dafür reicht es oft schon aus, mit so einer Session ID in der URL eine andere Seite zu besuchen. In dem Moment steht die SID im Referrer und landet somit direkt im Logfile des Zielservers. Schnappt sich jetzt jemand den Referrer aus dem Log und besucht die geschützte Seite, erhält er u.U. direkten Zutritt. Aus diesem Grund wird im Tutorial ja auch noch mehr über den Besucher abgefragt, als nur die SID und der Transport der Kennung per URL wurde per ini abgeschaltet.


    Ich hoffe das hat all deine Fragen beantwortet. ;)

  • Du schreibst, SessionCookie sollte man immer annehmen, im Firefox kann man die Cookies gar nicht so fein einstellen. Entweder alles oder nichts. Oder hab ich da was übersehen?


    Ich benutze die englische Version, daher die Bezeichnungen auf englisch. Unter Tools, Options, Privacy kann man generell einstellen ob man Cookies annimmt. Dabei kann man wählen zwischen der eigentlich aufgerufenen Seite (das sollte an sein), 3rd Party erlauben/blockieren (das sind Cookies von Fremdseiten, z.B. Werbepartner die in unsichtbaren IFrames geladen werden - sollte blockiert werden) und unter Exceptions (Ausnahmen), kann man dann nochmal sehr detailliert pro Domain einstellen, ob man alle Cookies blockieren möchte, erlauben möchte oder nur Session Cookies annehmen möchte.


    Dennoch, bei sensiblen Daten, die Serverseitig bestmöglich geschützt sein sollen, gibt es kein hätte, wäre, wenn. Da gibt es ein ganz klares entweder oder. Entweder der User benutzt die Seite zu den Bedingungen der Seite (Session Cookies erlauben) oder er benutzt sie gar nicht. Mittelwege oder Workarounds, nur es dem Benutzer so bequem wie möglich zu machen, führen fast zwangsläufig zu Sicherheitslöcher und Schwachstellen. Am Ende ist dann natürlich der User uneinsichtig und schiebt die Schuld dem Sitebetreiber zu. ;)