Loginpasswort richtig hashen - password_hash() & password_verify()

  • :D neeee. Ist ne Combi aus allem. Also Sonderzeichen ect. Für manche Sachen braucht man schon was sicheres.


    Zwecks sicherem Login:
    habe festgestellt, dass man keine $_SESSION verwenden darf, um die Loginfehlversuche mitzuschreiben.
    Das bring nämlich garnix, sobald da ein Tool (zB console) angreift.


    Also: Loginfehlversuche immer auf die Platte schreiben. zB in einer Tabelle (SQL).

  • So, ich habe mal ein wenig rumgespielt (mit gutem Vorsatz :D ) und dabei ein komplettes Loginskript zusammengeschraubt, welches (hoffentlich) alle heutztutage möglichen Sicherheitsschleusen eingebaut hat. ACHTUNG!!!: Bisher ungetestet!


    Was bereits inkludiert sein sollte:
    - Prüfen auf Vorhandensein der Eingaben
    - essentielle Daten (DB Verbindung, etc) in externem File (zum Ablegen in gesichertem Ordner)
    - Verbindung via PDO, um SQL Injection vorzubeugen
    - Password API + Pepper für sichere Passwörter
    - automatisches PW Update im Hintergrund
    - vollständige Fehlerfallbacks mit Abfangen der Meldungen


    Was noch eingebaut werden sollte:
    - gesichertes Dauerlogincookie setzen (zB via Token)
    - besserer Sessionumgang


    Was geprüft werden muss:
    - sämtliche oben genannten Punkte
    - Ajax-Kompatibilität
    - insg. die Hackingsicherheit


    Hier nun das Skript und die Konfig-Datei


  • Ich dachte schon Du willst jetzt hier ein kompletten Login posten :D
    ... das wäre bissl viel (also mit komplett meine ich forgot-pw-function, register, ect ect)


    Zeile 11 - kein Fehler, aber wenn User 0 eingibt, dann bekommt er den Fehler "nix eingegeben", obwohl ja eine 0 kam.
    Für empty() ist eine 0 leer - gibt also true zurück.


    Zeile 25 - ich mag die "?"-Platzhalter nich :D (musste raus :p )


    Zeile 28 - Im catch-block kommt nix an wenn User nicht existiert. Da kommt eher was an, wenn die Query einen Fehler hat (syntax ect).


    btw - warum ist es schlimm, wenn in einer SESSION Daten sind?


    Zeile 36 - wäre noch angebracht zu Prüfen, ob denn Daten vorhanden sind. Wenn die Query kein Ergebnis bringt - also kein User findet - dann bekommst Du dort eine "undefined $data ..." e-msg


    beginTransaction() - warum? Bei nur einer Query überflüssig. Und es erfolgt auch kein commit(). Und wieso $pdo = null; ?


    Zeile 46 - $query->execute($data); :: kann schief gehen sobald sich jemand die SELECT query umbaut. Dann ist in $data mehr als nur die gebrauchten Platzhalter-Keys und Du bekommst eine e-msg like "wrong number of bound parameter ..."


    Zeile 56 - "Falsches Passwort!" würde ich nicht preisgeben. Es ist denke ich besser mitzuteilen, dass "Benutzername und/oder Passwort falsch" ist.
    ^was uns wieder zum SELECT in der Usertbl bringt: wenn die SELECT query kein Ergebnis bringt, dann ist es mMn angebracht schon dem User mitzuteilen, dass "Benutzername und/oder Passwort falsch" ist.



    So ein Login ist sehr viel Arbeit und wenn man erstmal zu forgot-pw und co kommt, dann kann schon mal die Rübe qualmen.
    =)


    EDIT: mein "on login submit"-ablauf: http://pastebin.com/RpB22PZS
    viel Quark wegen nem kack Login =)

  • Nee, das was du da beschreibst, nenne ich nicht mehr Loginskript, das ist ja schon eine komplette Nutzerverwaltung. Ich dachte aber, der Login allein passt hier recht schön, um zu zeigen, wie man heutzutage (ungefähr) mit den Passwörtern speziell und der Sicherung allg. umgeht. Und ein Login ist immer noch eines der kürzesten Formulare, da nur zwei Werte (mit Dauerlogin drei) verarbeitet werden.


    Zeile 11: Ups, ok :whistling: Was schlägst du vor? !isset() ?
    Zeile 25: Kann sein, aber so ist es kürzer, da man kein assoziatives Array übergeben muss. Und da nur ein Wert kommt, kann man auch nichts durcheinander bringen. Deswegen mit ?
    Zeile 28: OK, dann gucke ich mal, wie ich die Userprüfung umbaue
    Zeile 36: Das hängt ja mit mit Z. 28 zusammen, ich bin davon ausgegangen, dass auch ein Fehler kommt, wenn nix gefunden werden kann
    Zeile 43: beginTransaction deshalb, weil rollBack nur geht, wenn vorher ein beginTransaction durchgeführt wurde. Und ich habe in Zeile 48 ein rollBack, damit lieber das nicht aufgewertete, komplette PW als ein aufgewertetes, halbes PW drinsteht. Denn ersteres funktioniert bei verify noch, letzteres nicht mehr.
    Zeile 46: Inwiefern die Select query umbauen? Wenn man die SELECT query umbaut, zB andere Spaltennamen, muss man das ja bei update auch machen. Und dann kann man noch kurz bei update die Platzhalternamen mit ändern, und es geht wieder, oder?
    $data ist ja ein ass. Array, mit Spaltennamen als Schlüssel und Zelleninhalt als Wert. Dh, wenn man den Spaltennamen als Platzhalter wählt, klappt das mit $data eigentlich immer, imho.
    Zeile 56: Nun denn, dann kann man das mit der neuen Nutzerprüfung zusammen legen.


    Session: ist nicht so schlimm, aber ich finde es einfach angenehmer, und ist auch leichter zu prüfen und schwerer zu manipulieren. Wenn man immer nur ein if(session) fährt, ist egal, was mitgeliefert wird, es wird immer auf true oder false runtergebrochen, aber sobald man werte prüfen will, müssen diese auch sanitized werden. Oder habe ich da nen Denkfehler?


    Idee, zu prüfen: Wie läuft es eigentlich, wenn man zB selbst eine Funktion baut und diese in die Session setzt? Wird dann bei if($session) die Funktion ausgeführt?
    Wenn die $session zB ein

    PHP
    function() {
        echo $dbpass;
    }

    ist, ist dass ja immer noch true, wird das dann aber auch ausgeführt?


    $pdo = null beendet einfach die PDO DB-Verbindung, ich bin ordentlich :D

  • Zeile 11: jo, hab ich vergessen sorry: am besten das gute alte

    PHP
    if($user == '' || $pass == '')


    oder (was ich besser finde)

    PHP
    if(strlen($user) < *mindestWertUser*) {...} 
    if(strlen($pass) < *mindestWertPass*) {...}


    Ich glaub ich hab vorhin $pdo->commit() übersehen :D sorry.


    Zeile 46: was ich meinte ist: wenn jemand sich zusätzlich noch die (SELECT ... ) `id` holt, dann würde die $data['id'] auch mit bei $pdo->execute($data) auftauchen.
    Dadurch werden 2 Werte (pwhash, username) erwartet, aber 3 reingeschickt. Also wäre nur fürs Verständnis das hier einfacher:

    PHP
    $parameter = array(
     'pwhash' => $data['pwhash'],
     'username' => $data['username']
    );
    $query->execute($parameter);


    Zitat

    aber sobald man werte prüfen will, müssen diese auch sanitized werden. Oder habe ich da nen Denkfehler?


    Die Werte kannst Du so, wie sie in der SESSION liegen, nutzen. Oder verstehe ich jetzt was mit sanitize falsch? :D


    Ne function in einer SESSION?
    Du meinst sowas?:

    PHP
    session_start();
    if(!isset($_SESSION['a'])){
        $_SESSION['a'] = function(){
            echo '<br>in function';
        };
    }
    if($_SESSION['a']){ # die function wird hier nicht ausgeführt
    }
    echo '<br>call $_SESSION[\'a\']();';
    $_SESSION['a']();


    output:

    Zitat

    call $_SESSION['a']();
    in function
    Fatal error: Uncaught exception 'Exception' with message 'Serialization of 'Closure' is not allowed' in [no active file]:0 Stack trace: #0 {main} thrown in [no active file] on line 0


    Ich versteh grad nich, was Du mit der SESSION anstellen willst =)

  • Also, ich habe einen leeren String eingebunden, und du hast das commit nicht übersehen, das hatte ich wirklich vergessen.


    Und warum sollte man noch die ID wollen, es geht doch hier nur um den Login, da braucht man nur Namen und PW? Mehr kriegt man vom Nutzer ja auch nicht?!
    Aber ich kann es natürlich auch ändern.


    Und bei der Session meinte ich, dass sich zum Bsp ein pöser Pube registrieren könnte, einloggen und dann die Session auf seinem Rechner so manipulieren, dass sie nicht mehr einfach nur true enthält, sondern eine Funktion.
    Also $_SESSION['login'] = true wird zu $_SESSION['login'] = function(){echo dbuser . " " . $dbpass;}
    Wenn man nun fragt if($_SESSION['login']), wird dann die Funktion in der manipulierten Session ausgeführt?

  • Die isset() Afragen müssen bei der Zuweisung der $_POSTs hin. Die Eingabefelder sollten zwar immer da sein, aber ... =)
    Also if(isset($_POST['user'])){$user = ...}
    Und ab hier ist ja $user immer gesetzt. Wäre dann also nur noch das prüfen auf $user == "" || $pass == ""


    Das mit der id war nur ein Bsp. Kann ja sein, dass jemand noch `zuletzt_gesehen` usw speichern (und gleich mit auslesen) will.


    Zwecks SESSION:
    die wird nicht ausgeführt, solange Du nicht $_SESSION['login'](); aufrufst (mit den Klammern).
    btw: der Fatal Error ist ein Bug: https://bugs.php.net/bug.php?id=64168 - wieder einen gefunden.


    Bei einem if($_SESSION['login']) wird also nur der Zustand geprüft.
    Allerdings - wenn nun jemand die SESSION bearbeiten könnte, dann bräuchte er in dem Script nur "login" auf true setzen und wäre eingeloggt.
    Und beim wiederaufrufen der Page würde ja nun via SESSION geprüft werden, ob denn User eingeloggt ist ($_SESSION['login'] === true) -- aber es sind keine Daten gelesen/vorhanden.
    Der eingeloggte User ist also ein Ghost :D
    Es muss also min. eine id in die SESSION.


    Ich behandle SESSION genauso wie Cookies. Heißt: da ist (u.a.) die id und pwhash des Users drin. Sind beide korrekt ($dbhash == $hash), ist user eingeloggt.
    Wenn denn nun User die SESSION bearbeiten kann (mir unbekanntes Land), dann kann er nur das gleiche machen wie auf der Seite selbst: username und pw raten/bruteforcen/ect.
    (Daher auch falsche SESSION und Cookie als falschen Loginversuch zählen)

  • Hmm, das heisst dann aber, dass ggf. auf jeder einzuschraenkenden Seite erstmal eine DB Abfrage gestartet werden muss, um den PW Hash zu kriegen, und dann pruefen zu koennen.
    Da finde ich ein einfaches true besser :)


    Aber irgendwie hast du mit deinem Ghostuser auch recht. Hmm, verzwickt. :(


    Idee: $_SESSION["login"] = uniqid("", true);
    Dann kann man in Reverse Engineering die (fast) genaue Zeit auslesen:

    PHP
    //Verdammter Sonnenbrillensmylie
    date("r",hexdec(substr(uniqid(),0,8)));


    Und muss nur noch irgendwo die durch das true generierten Pseudozufallszahlen speichern und abgleichen. Dadurch hat man eine Idee, wann die Session gestartet wurde, und aknn zB nach 2 Std Inaktivtaet rausschmeissen, und gleichzeitig sind die Werte nicht effizient reproduzierbar/wiederverwendbar.
    Einziges Problem: Wo speichern, damit die Abfrage moeglichst schnell geht, da ja immer noch auf jeder Seite gefragt werden muss?


    Andere Moeglichkeit, fuer Fortgeschrittene: PHP Frameworks. Die groessten, bzw. empfohlensten derzeit: Phalcon, Yii, Laravel, CodeIgniter, Symfony. Die sollten alle eine gute Verwaltung mitbringen.
    Zumindest mit Phalcon habe ich mich schonmal ein wenig beschaeftigt, wenn man sich einmal eingearbeitet hat, sind die Viecher wirklich enorm hilfreich!!! Quasi CMS Systeme fuer nativ-Coder xP

  • Die genaue Zeit bekommst Du noch einfacher:
    $_SESSION['created'] = time(); :D


    Dadurch hat man eine Idee, wann die Session gestartet wurde, und aknn zB nach 2 Std Inaktivtaet rausschmeissen


    Standard ist 1440 sec (24 min) session life time. Aber klar - kann man ja ändern.


    Einziges Problem: Wo speichern, damit die Abfrage moeglichst schnell geht, da ja immer noch auf jeder Seite gefragt werden muss?


    DB =) Dafür is die ja nun mal da. Und im normal Fall um die 0.005 Sec (bei persistant Verbindung, sonst evtl ne halbe Sec connection time bei jedem Seitenaufruf).


    Ich weiß auf was Du hinaus willst. Mir fehlt bei der ganzen Server/PHP/Client-Sache auch irgendwie der "Laufende Betrieb".
    Bei jedem Seitenrefresh ist ja nun mal der User ganz am Anfang des Scripts praktisch unbekannt/neu/...
    Aber das wird sich nicht ändern. Man wird immer wieder aufs neue prüfen müssen, wer (und was) da reinkommt :D

  • EDIT: was ich hier vorher gepostet hatte war falsch - daher gelöscht.
    Ich hatte das mal von einer Seite im Netz aufgeschnappt ... man sollte doch intensiver googlen wenns um Sicherheit geht =)


    Also - SESSIONS. Wo landen die Daten?
    Antwort: Serverseitig! Der User kommt also nicht an die Daten ran. Es wird nur eine SESSION-ID beim User hinterlegt*, welche dann zur Identifizierung genutzt wird.
    * entweder per SESSION-Cookie (Cookies müssen akzeptiert werden), oder per URL (?PHPSESSID=...)


    Was kann also passieren?
    Antwort: Man kann die SESSION stehlen, indem man die SESSION-ID hat/klaut/kennt/...


    Was sollte man bei seinen Tools/Scripten für SESSION-Einstellungen nutzen?
    Antwort:

    PHP
    ini_set('session.use_cookies','1');  
    ini_set('session.use_only_cookies','1');  
    ini_set('session.use_trans_sid','0'); 
    ini_set('session.cookie_httponly','1'); 
    session_start(); 
    session_regenerate_id(true);


    Was bedeutet das im Detail?
    Antwort:
    Sehr gut erklärt: http://www.d-mueller.de/blog/p…sion-management-erklaert/
    session.use_cookies() http://www.php.net/manual/de/s…p#ini.session.use-cookies
    session.use_only_cookies() http://www.php.net/manual/de/s….session.use-only-cookies
    session.use_trans_sid() http://www.php.net/manual/de/s…ini.session.use-trans-sid
    session.cookie_httponly() http://www.php.net/manual/de/s…i.session.cookie-httponly
    session_regenerate_id() http://www.php.net/manual/de/s…i.session.cookie-httponly


    Zu session_regenerate_id():
    Man sollte session_regenerate_id(true) (also boolean true) mitgeben, damit die "alten" SESSIONs gelöscht werden.
    Sonst sammeln sich sinnlos veraltete, nicht mehr verwendete SESSSIONs auf dem Server.


    Weiteres zum Thema SESSION:
    http://www.php-kurs.com/session-hijacking.htm
    http://www.php.net/manual/de/intro.session.php
    http://www.php.net/manual/de/session.security.php


    The Scout
    Man kann also die SESSION['login'] = true nicht manipulieren. Es sei denn jemand hat Zugriff auf die Server-Files. Aber dann ist es ja sowieso zu spät (und der Angreifer braucht die SESSIONs nicht mehr =).

  • D.h. mit der Konfig reicht auch das Session=true, um die Leute eindeutig via deren ID zu identifizieren. Man muss nur darauf achten, dass diese ID nicht so leicht in falsche Haende geraten kann.

  • pepper ändern: nein. Der muss immer gleich bleiben.
    Und wie gesagt: die SESSION-Daten sind auf dem Server. Nur wer die SESSION-id in die Finger bekommt, kann sich als User-x ausgeben.
    Um dann noch an die Daten ranzukommen, müsste der Angreifer wohl (ungetestet) noch weiter gehen: man müsste das Script dazu bringen die Daten anzuzeigen.


    Einfach mal testen: erstelle eine SESSION, und gehe in den Ordner C:\xampp\tmp (wenn xampp genutzt wird).
    Sortiere nach "Änderungsdatum" und öffne die zuletzt geänderte Datei im Editor (Datei sieht in etwa so aus: sess_fdnphmbs70qjn0hj7cpbh7isf2)
    Der String der da drin steht kann decodiert werden mit unserialize().
    Aufbau:
    | == trenner ( key|value )
    s:8 == string von 8 Zeichen
    i:1 == int 1 zeichen
    N == null
    b == bool
    ...

  • Hmm, wäre mal interessant, pepper ändern >:-)
    "Wie sperre ich sämtliche User gleichzeitig auf möglichst effiziente Weise aus?"


    Also, Pepper nur ändern, wenn der Server gehackt wurde *hust*Heartbleed*hust* und nur mit roter Neonschrift einmal quer über den Login xP


    Und für die Session ID muss der Angreifer ja im Grunde nen Tojaner haben oder man in the middle spielen?! Also, bei Wichtigem: immer schön SSL mit PFS (Perfect Forward Secrecy) nutzen :thumbup:

Jetzt mitmachen!

Sie haben noch kein Benutzerkonto auf unserer Seite? Registrieren Sie sich kostenlos und nehmen Sie an unserer Community teil!