Mueller: header(Location: ...) mit unerwartetem Ergebnis

Hi,

ich habe ein Problem mit einem Login-Skript, bei dem sich ein Nutzer mit Name und Passwort anmelden kann. Hierzu wird in einer DB u.a. die Session-ID gespeichert. Zusätzlich wird die Nutzer-ID in einer $_SESSION Variable abgelegt. Ist die Anmeldung erfolgreich, soll der Nutzer direkt per header(Location: ...) auf eine Zielseite weitergeleitet werden.

Das Problem: kommt er auf dieser Seite an, erscheint es als wäre er nicht angemeldet. Erst wenn die Seite refreshed wird, erkennt das Skript den Nutzer.

Beim Seitenaufruf prüfe ich lediglich, ob die Nutzer ID aus der $_SESSION Variable in Verbindung mit der Session-ID in der Datenbank abgelegt wurde.

Setze ich vor dem header()-Aufruf den Befehl sleep(1) ein, dann funktioniert die Weiterleitung richtig und der Nutzer wird auf der Zielseite als solcher erkannt. Es scheint also ein Art Zeitverzögerung vorzuliegen.

Dies ist sehr merkwürdig, da das PHP Skript ja erst weiterläuft, wenn die Session-ID tatsächlich in die DB geschrieben wurde bzw. die User-ID tatsächlich in die $_SESSION Variable geschrieben wurde.

Hat jemand vielleicht eine Idee, wie es zu diesem Verhalten kommen könnte? Ursprünglich hat das Anmeldeskript schon mal funktioniert, seit ich jedoch auf PDO umgestellt habe nicht mehr. Kann aber auch nur Zufall sein!

Vielen Dank vorab für jede Antwort!

  1. Ahoi,

    wird denn bei einem Redirect, oder was ist header(Location...) ein neuer Request ausgeführt, bei dem dann auch das geschriebene Cookie für die Sesssion erscheint?

    Dank und Gruß,

    frankx

    1. Hi,

      wird denn bei einem Redirect, oder was ist header(Location...) ein neuer Request ausgeführt, bei dem dann auch das geschriebene Cookie für die Sesssion erscheint?

      Das Session Cookie müsste von einem Redirect unberührt bleiben. Die neue Seite wird aufgerufen und kann dann, sofern (in meinem Fall) Cookies beim Client erlaubt sind, direkt auf die Session Daten wieder zugreifen

      Dies funktioniert bei mir ja auch, allerdings nicht direkt nach der Weiterleitung, sondern erst nach einem erneuten Seitenaufruf. Demnach müssen doch alle notwendigen Daten vorliegen, jedoch >> noch nicht << zum Zeitpunkt des header()-Aufrufs!?!?

      Gruß
      Mueller

      1. Ahoi,

        Dies funktioniert bei mir ja auch, allerdings nicht direkt nach der Weiterleitung, sondern erst nach einem erneuten Seitenaufruf. Demnach müssen doch alle notwendigen Daten vorliegen, jedoch >> noch nicht << zum Zeitpunkt des header()-Aufrufs!?!?

        Daher die Frage, ob das Cookie schon übermittelt wird an den Server. Wie wärs mit einem var_dump($_REQUEST)?

        Dank und Gruß,

        frankx

        1. Moin!

          »» Dies funktioniert bei mir ja auch, allerdings nicht direkt nach der Weiterleitung, sondern erst nach einem erneuten Seitenaufruf. Demnach müssen doch alle notwendigen Daten vorliegen, jedoch >> noch nicht << zum Zeitpunkt des header()-Aufrufs!?!?

          Daher die Frage, ob das Cookie schon übermittelt wird an den Server. Wie wärs mit einem var_dump($_REQUEST)?

          Warum $_REQUEST dumpen, wenn man Cookies nachgucken will? Lieber $_COOKIE dumpen... :)

          - Sven Rautenberg

          1. Ahoi,

            »» Daher die Frage, ob das Cookie schon übermittelt wird an den Server. Wie wärs mit einem var_dump($_REQUEST)?

            Warum $_REQUEST dumpen, wenn man Cookies nachgucken will? Lieber $_COOKIE dumpen... :)

            Weil die Session u.U. nicht mit dem Cookie daherkommt?

            Dank und Gruß,

            frankx

            1. Hi,

              »» Daher die Frage, ob das Cookie schon übermittelt wird an den Server. Wie wärs mit einem var_dump($_REQUEST)?

              Warum $_REQUEST dumpen, wenn man Cookies nachgucken will? Lieber $_COOKIE dumpen... :)

              Weil die Session u.U. nicht mit dem Cookie daherkommt?

              Womit sonst - per GET übergebener Session-ID?
              Die hängt PHP beim Location-Header nicht selbstständig dran.

              MfG ChrisB

              --
              „This is the author's opinion, not necessarily that of Starbucks.“
              1. Ahoi,

                Womit sonst - per GET übergebener Session-ID?
                Die hängt PHP beim Location-Header nicht selbstständig dran.

                Genau, und dann würde man u.U. mit $_REQUEST sehen, dass bei dem Setzen des Location-Headers vielleicht garkeine Session-Id im ganzen $_REQUEST-Array vorhanden wäre, erst nach dem (Browser)Refresh, weil der dann nämlich entweder das Cookie schickt oder eben per Get oder Hidden-Post-Feld. So war der Hintergedanke, weil ich eben nicht probiert habe, das Setzen eines Location-Headers im Script vielleicht so die Session "vergisst" mitzuliefern.

                Dank und Gruß,

                frankx

                1. Hi,

                  So war der Hintergedanke, weil ich eben nicht probiert habe, das Setzen eines Location-Headers im Script vielleicht so die Session "vergisst" mitzuliefern.

                  Die Nutzerkommentare zu session_start im Manual halten auch noch ein paar gute Infos zu dieser Problematik bereit.

                  Speziell dieser Hinweis könnte im vorliegenden Falle vielleicht hilfreich sein.

                  MfG ChrisB

                  --
                  „This is the author's opinion, not necessarily that of Starbucks.“
                  1. Ahoi,

                    Speziell dieser Hinweis könnte im vorliegenden Falle vielleicht hilfreich sein.

                    Das sieht ja so aus, als wenn sich das Schreiben der Sessiondaten eben soweit verzögern kann. Deshalb hilft auch das erwähnte sleep() scheinbar.

                    Dank und Gruß,

                    frankx

                    1. Hi,

                      erstmal vielen Dank für die zahlreichen Kommentare zu meinem Problem!!!

                      Ich habe mal die verschiedenen Hinweise auf php.net ausprobiert, u.a. auch mit session_write_close() oder das Setzen von session.use_only_cookies in der php.ini. Leider hat nichts davon funktioniert, auch nicht in Kombination.

                      Dann habe ich mir über var_dump($_REQUEST) und var_dump($_COOKIE) mal die ganzen Variablen aus meinem Login-Skript ausgeben lassen und siehe da, keine der $_SESSION Variablen, die ich vorher gesetzt habe, erscheint! Lediglich PHPSESSID. Allerdings erscheinen die Variablen auch dann nicht, wenn ich die Seite mit einem Refresh aktualisiere (und das Auslesen der Variablen dann ja klappt)!!

                      Viele Grüße
                      Mueller

                      1. Nachtrag: Habe jetzt auch mal gezielt die $_SESSION Variable ausgeben lassen, die ich beim Login setze - sie ist vorhanden!!! Demnach liegt das Problem nicht an der Session, sondern an dem MySQL INSERT. Ich speichere nämlich u.a. die IP-Adresse in einer Datenbank, die ich dann bei jedem Seitenaufruf abfrage. Vermutlich erfolgt die erste Abfrage nach dem Refresh noch bevor die IP-Adresse tatsächlich in die Datenbank geschrieben wurde. Grund könnte vielleicht caching o.ä. sein?

                        1. Nach weiteren Tests kann ich sagen, dass es eindeutig an dem Schreiben der IP-Adresse in die Datenbank liegt - dies geschieht einfach nicht schnell genug, sodass sie beim Seitenaufbau noch nicht vorliegt.

                          Wie kann man erzwingen, dass eine Abfrage vollständig ausgeführt wird, bevor man mit dem Skript fortfährt?? Der Aufruf von sleep erscheint mir als unzureichend, da die Zeit ja auch von der Auslastung des Servers abhängt und ein sleep(1) vermutlich nicht immer ausreichen wird.

                          Viele Grüße
                          Mueller

                          1. 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.

                            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.

                            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!

                            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 :-)

                            Dennoch herzlichen Dank für eure Mithilfe beim Eingrenzen des Problems! Sonst wäre ich nie hierauf gestoßen!

                            Viele Grüße
                            Mueller

                            1. Ahoi,

                              Dennoch herzlichen Dank für eure Mithilfe beim Eingrenzen des Problems! Sonst wäre ich nie hierauf gestoßen!

                              Und Danke für Deine Rückmeldungen!

                              Dank und Gruß,

                              frankx

                            2. 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

                              1. Hallo,

                                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.

                                Hm, ich habe eigentlich nichts am Session Handling verändert - der Server speichert sie nach wie vor in Dateien. Die Datenbank nutze ich lediglich dazu um zusätzliche Informationen zur Nutzererkennung abzuspeichern. Damit dürfte es doch zu dem angesprochenen Problem der Race-Conditions gar nicht kommen?

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

                                Die IP ist ja nicht der einzige Bestandteil meiner Prüfung, sondern nur einer von vielen, wie z.B. User-Agent etc. (genau wie du sagtest). Der Fall, dass mehrere Personen z.B. über einen Proxy ins Netz gehen sehe ich daher als unproblematisch an, zumal sie ja im Regelfall dann auch unterschiedliche Sessions haben.

                                Zweitens: Ein User kann mehrere IPs haben.

                                Das ist mir neu. Ist es tatsächlich möglich, dass während einer Session bei jedem neuen Request eine andere IP-Adresse verwendet wird? In welchen Situationen könnte dies auftreten?

                                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. ;-)

                                Hast ja recht ;-)

                                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.

                                Ist nicht der einzige Beleg. Wie gesagt nutze ich die Sessions "ganz normal". D.h. ich speichere in den Session-Daten zum Beispiel die User ID. Beim Seitenaufbau prüfe ich dann zunächst, ob für diese User ID in der Datenbank vermerkt ist, dass sie sich angemeldet hat (in Kombination mit den weiter oben angesprochenen Merkmalen wie User-Agent etc.).

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

                                Genau das mache ich ja :-) Nach einer gewissen Inaktivität des Nutzers muss er sich erneut anmelden um fortzufahren.

                                Viele Grüße
                                Mueller

                                1. Moin!

                                  »» 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.

                                  Hm, ich habe eigentlich nichts am Session Handling verändert - der Server speichert sie nach wie vor in Dateien. Die Datenbank nutze ich lediglich dazu um zusätzliche Informationen zur Nutzererkennung abzuspeichern. Damit dürfte es doch zu dem angesprochenen Problem der Race-Conditions gar nicht kommen?

                                  Möglicherweise dann nicht. Die Frage ist allerdings, warum du zum Speichern von Sessiondaten nicht $_SESSION nimmst, sondern extra noch die Datenbank. Zumindest anhand der gegebenen Informationen sieht es eher so aus, als ob das verzichtbar sein sollte.

                                  »» Zweitens: Ein User kann mehrere IPs haben.

                                  Das ist mir neu. Ist es tatsächlich möglich, dass während einer Session bei jedem neuen Request eine andere IP-Adresse verwendet wird? In welchen Situationen könnte dies auftreten?

                                  Wenn der User über eine Proxy-Farm mit Load-Balancing geleitet wird, kriegt er u.U. bei jedem Request eine neue IP. Ebenso könnte sich natürlich auch sein Router zwischendrin neu connecten und er eine neue dynamische IP zugewiesen bekommen.

                                  Unter dem Strich: Die Prüfung der konstanten IP ist eher ungeeignet.

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

                                  Genau das mache ich ja :-) Nach einer gewissen Inaktivität des Nutzers muss er sich erneut anmelden um fortzufahren.

                                  Diese Info schreibt man aber normalerweise, genausowenig wie den Anmeldestatus, persistent in die Datenbank, sondern lässt sie in den Sessiondaten.

                                  - Sven Rautenberg

                                  1. Hi Sven,

                                    Die Frage ist allerdings, warum du zum Speichern von Sessiondaten nicht $_SESSION nimmst, sondern extra noch die Datenbank. Zumindest anhand der gegebenen Informationen sieht es eher so aus, als ob das verzichtbar sein sollte.

                                    Da hast du natürlich Recht, allerdings stecken hier auch Überlegungen zu Nutzerstatistiken etc. dahinter, was ein Speichern in der Datenbank voraussetzt.

                                    Wenn der User über eine Proxy-Farm mit Load-Balancing geleitet wird, kriegt er u.U. bei jedem Request eine neue IP. Ebenso könnte sich natürlich auch sein Router zwischendrin neu connecten und er eine neue dynamische IP zugewiesen bekommen.

                                    Okay, sowas habe ich bislang nicht wirklich berücksichtigt. Dazu werde ich mir wohl noch ein paar Gedanken machen müssen!

                                    Danke und Gruß
                                    Mueller