Sven Rautenberg: header(Location: ...) mit unerwartetem Ergebnis

Beitrag lesen

Moin!

Problem gelöst: die Überprüfung der IP-Adresse erfolgte bei mir mit einem UPDATE, da ich bei Erfolg gleichzeitig den Zeitpunkt der letzten Nutzeraktivität speichere.

Es sei im Zusammenhang mit dem Speichern von Sessiondaten in der Datenbank darauf hingewiesen, dass du dir damit Race-Conditions ins Haus holst, ohne es zu wissen.

Der Standard-Save-Handler bei Sessions speichert in normale Dateien im Dateisystem und nutzt Locking zur Sperrung. Während ein PHP-Skript läuft und die Session-Daten durch session_start() eingelesen und im Array $_SESSION gespeichert hat, und dort auch Änderungen durchgeführt werden, kann kein anderes Skript parallel session_start() durchführen, sondern muss warten, bis die (aktualisierten) Sessiondaten in $_SESSION entweder durch das Skriptende oder durch session_write_close() wieder auf die Platte geschrieben wurden.

Dieses Verhalten garantiert, dass in $_SESSION immer "korrekte" Daten enthalten sind, die sich in der Reihenfolge der Abarbeitung der Skripte weiterentwickeln. Wobei die Abarbeitungsreihenfolge nicht zwingend der Reihenfolge der Requests beim Server entsprechen muss: Wenn zwei Skripte auf das Freigeben des Datei-Locks warten, würde ich es eher als zufällig betrachten, welches der beiden Skripte zuerst freigegeben wird.

Der Wechsel der Sessionspeicherung von Datei zu Datenbank lässt das Locking erstmal wegfallen, wenn man es nicht explizit in den eigenen Speicher- und Lesefunktionen implementiert.

Tut man es nicht, holt man sich wahrscheinlich diverse Race-Conditions ins Haus, von denen man erstmal keine Ahnung hat. Das unschöne an Race-Conditions ist ja, dass sie absolut nicht offensichtlich sind, sondern sich erst durch merkwürdige, unerklärliche Effekte bei hoher Serverlast bzw. in Bezug auf die eigenen Session-Handler bei mehreren parallelen Requests in derselben Session zeigen.

Folgendes Vorgehen erwies sich nun als fehlerhaft:

  • Die letzte Nutzeraktivität wird beim Login auf NOW() gesetzt.
  • Beim Überprüfen der IP nach dem Seitenaufruf ebenfalls wieder auf NOW().
  • Anhand der affected rows weiß ich dann, ob der Nutzer angemeldet ist.

Das Prüfen der IP ist schon mal keine so gute Idee. Erstens: Mehrere User können dieselbe IP haben. Zweitens: Ein User kann mehrere IPs haben.

Wenn überhaupt Annahmen getroffen werden können, die Session-Hijacking erschwerden, dann diese: Ein User wird innerhalb seiner Session vermutlich keinerlei Veränderung an dem einmal gewählten User-Agent vornehmen können. Ebenso dürften die weiteren vom Browser gesendeten Request-Bestandteile wie akzeptierte Sprache, akzeptierte Medientypen etc. konstant sein.

Allerdings ist der MySQL Server so schnell, dass NOW() in beiden Fällen exakt den selben Timestamp lieferte und somit das UPDATE keinen Effekt hatte und affected rows auf 0 gesetzt wurde!

Nun ja, wer konnte auch schon damit rechnen, dass MySQL das dokumentierte Verhalten für affected_rows zeigt, und außerdem noch weniger als eine Sekunde für die Ausführung eines Querys benötigt. ;-)

Das erklärt auch, warum der sleep(1) Befehl funktionierte -> er führte lediglich dazu, dass das 2. NOW() mindestens eine Sekunde zeitversetzt war. Das habe ich jetzt direkt in das SQL Statement über NOW() + 1 eingebaut -> Problem gelöst :-)

Ich finde es ja eher fragwürdig, dass das Auffinden einer gespeicherten IP der einzige Beleg dafür sein soll, dass ein User eingeloggt ist, oder nicht. Die übliche Vorgehensweise wäre doch eher, dass das Skript die Sessiondaten einlädt und dort einen gespeicherten Status auffindet. Wenn der auf "eingeloggt" steht, weil zuvor die übermittelten Benutzerdaten als korrekt erkannt wurden, oder wenn die in der Session gespeicherten Benutzerdaten immer noch mit der Benutzerdatenbank übereinstimmen, sollte der Benutzer als eingeloggt erkannt werden.

Der Zeitpunkt der letzten Aktivität ist lediglich dann von Interesse, wenn es um die Realisierung eines sicheren Auto-Logouts bei Inaktivität geht.

- Sven Rautenberg