PHP und Sessions

PHP hat ein paar kleine Hämmer (sprich, riesengroße Fehler) im Session-Management, die man leider nicht so einfach beseitigen kann:

  • PHP ist nicht in der Lage, eine zufällige Session-ID zu generieren, ohne dass man eine Session startet.
  • Wird eine Session gestartet erzeugt PHP keine zufällige Session-ID, falls schon eine vom Browser gemeldet wird.
  • Wird eine Session gestartet werden automatisch Header ausgegeben um das Session-Cookie zu setzen. Das kann man nicht unterdrücken.
  • Man kann keine Session starten, wenn man keine Header mehr an den Browser schicken kann.
Also halten wir fest:

  • Man hat keine Möglichkeit, Zufallswerte zu erzeugen.
  • Eine Session zu starten reicht in der Regel eben nicht aus, um eine Session zu starten.
  • Das Starten einer Session muss so früh auf einer Seite geschehen, dass man meistens noch gar keine Möglichkeiten dazu hat, ordentlich einzugreifen.
  • Außerdem hat session_start() zillionen verschiedene Seiteneffekte, wie z. B. evtl. das Setzen von irgendwelchen globalen Variablen, darunter dem Wort SID.

Die Session-Implementierung in PHP ist an sich vollkommen unbrauchbar

session_start() in PHP ist so ziemlich mit das mieseste Stück einer Implementierung in einem weltweiten De-Facto-Standard der mir jemals in meiner Laufbahn untergekommen ist. Man muss extrem vorsichtig sein und unheimlich viel Seiteneffekte bedenken, um sicher zu sein, dass man durch session_start() nichts kaputtmacht.

Außerdem ist session_start() im Zusammenhang mit Sicherheit ein wahrer Horrortrip.

Probleme

Web-Services überschreiben gegenseitig Variablen

  • Auf einer Maschine gibt es zwei verschiedene Web-Services, nennen wir sie http://maschine/a/ und http://maschine/b/
  • Beide Services verwenden Sessions
  • Ruft man nun beide Services nacheinander auf, hat man nicht zwei Sessions offen, sondern in Wirklichkeit nur eine, was an sich der richtige Weg ist.
  • Benutzen nun die beiden Web-Services denselben Variablennamen in der Session, überschreiben sich gegenseitig die Daten.
Es gibt folgende Lösungen:
  • Man setzt für die Web-Services verschiedene Namensräume (z. B. über Variablen-Prefixe), was die empfehlenswerte Variante wäre. Wie immer unterstützt PHP den logischerweise sinnvollsten Ansatz nicht. Da man Code ja wiederverwenden will muss man davon ausgehen, dass der Variablenname, der in der Session registriert wird, sich eben nicht von dem in einem anderen Web-Service unterscheidet. Durch ziemlich komplizierten selbstmodifizierenden Code (eval usw.) kann man das Problem allerdings umgehen, oder man verwendet eben keine Variablen.
  • Man verwendet verschiedene Session-Namen (session_name("webservice")), allerdings sind die verwendbaren Namen eingeschränkt. Ich habe noch nicht herausbekommen welche Zeichen funktionieren und welche nicht, am Besten nimmt man also nur Namen die aus den Buchstaben a bis z bestehen. Der Nachteil ist, dass dadurch zwei Sessions generiert werden. Der Nachteil besteht also im Browser und im Server, da dieser dann das Mehrfache an Sessions verwalten muss.
  • Man verwendet session_set_cookie_params() um den Session-Pfad zu setzen. Das geht aber grundsätzlich nicht, wie jeder Möchtegern-Web-Anfänger schnell feststellen wird sobald er mit PHP anfängt. Denn welcher Pfad (wie viele Ebenen) soll da bitte automatisch eingetragen werden? Was ist mit Web-Services die in die Pfade anderer Web-Services integriert werden? Und was ist mit Anwendungen, deren Root-Pfad sich ständig ändern soll? Diese Lösung ist außerdem filigran, kompliziert, Fehleranfällig und generell nicht empfehlenswert, außerdem krankt es an derselben Problematik wie der vorherige Punkt.
  • Man verwendet session_set_save_handler(). Sprich, man ersetzt die Core-Funktionen von PHP durch etwas, das funktioniert. Ein ebenfalls PHP-typischer Ansatz.
  • Man löst das Problem, indem man session_register() niemals verwendet. Das ist übrigens eine PHP-typische Lösung, nämlich das, was die Sprache bereitstellt, nicht zu verwenden, da es vollkommen hirntot implementiert wurde.
Es gibt also zwei prinzipielle Methoden das Session-Management zu korrigieren:

  • Man macht es vollständig selber, baut also nur auf der Session-ID auf.
  • Man repariert die in PHP eingebauten Fehler durch komplizierte bis komplizierteste Workarounds.
Die Reparatur wäre dabei durch Einführung einer einfachen PHP-Funktion session_set_prefix("prefix") extrem einfach und billig. Alles was in PHP dann geändert werden müsste ist, dass jede Variable beim Speichern in der Session mit diesem Prefix versehen würde, beim Laden würde der Prefix wiederum entfernt. Falsche Prefixe würden nicht entfernt und stören somit nicht, Variablen ohne Prefix müssten von denen mit Prefix überschrieben werden.

Selber kann man das leider nicht leicht einbauen, da PHP nicht erlaubt, in session_set_save_handler() auf die vorhandene Implementation zurückzugreifen (man kann builtins nicht wrappen und fixen, ebenfalls ein grundsätzlicher Designfehler in PHP).

Die Session-ID ist unsicher

  • Die Session-ID wird vom Browser mitgeteilt
  • Wenn der Browser eine Session-ID mitteilt wird diese von PHP verwendet.
Das bedeutet, die Session-ID ist unsicher. Eigentlich müsste eine Session-ID ein kryptologischer Schlüssel sein den man nicht vorhersagen kann. Da die Session eine zentrale Rolle im Bereich der Sicherheits-Managements ist (ohne Session gibt es keine Logins da HTTP stateless arbeitet, also selber keine Sessions kennt) darf eine Session-ID nicht vom Anwender (möglichen Angreifer) vorgegeben werden können.

Aber leider kann man bei PHP bestimmte Session-IDs leicht erraten. Beispielsweise wird davon berichtet, dass sehr oft die Session-ID "deleted" vorkommt, was durch mehrere Fehler in PHP (1. festgelegte Session, 2. Datum das von der korrekten Systemzeit auf beiden Seiten abhängt) verursacht wird. Genau solche und ähnliche Session-IDs lässt PHP aber ungeprüft zu.

Richtig wäre, die Session-ID aus mehreren Teilen aufzubauen, so dass der Server erkennen kann, ob er (und nur er) diese ID auch wirklich generiert hat, so dass die ID automatisch ungültig ist, wenn z. B. das Cookie mal an den falschen Server geschickt wird (auch das ist gar nicht selten). Ein ebenso brauchbarer Workaround wäre z. B. die Session automatisch zu regenerieren, wenn sie unbekannt ist (also z. B. ausgetimet ist).

Vom kryptologischen Standpunkt aus ist nur die erstere Lösung die einzig richtige. Deshalb ist verwunderlich, dass es selbst nach so vielen Jahren nicht in PHP implementiert wurde. Die sekundäre Lösung ist weitgehend brauchbar und sehr einfach, aber auch dies wurde bisher anscheinend ebenfalls nicht in PHP integriert.

Leider lässt sich beides wegen mangelnder Unterstützung durch die Sprache per Workarounds in PHP nicht bzw. so gut wie nicht nachrüsten, im PHP-Core wäre es vermutlich mit einer einzigen zusätzlichen Codezeile leicht erledigt. Daraus kann ich an sich nur folgern, dass die PHP-Entwickler bis heute nicht wissen, was sie tun.

Es gibt aber einen weiteren Workaround, den ich im Folgenden vorstelle.

Beim Aktivieren einer Session darf man sich nicht auf bestehende Sessions verlassen

Da die Session-ID also unsicher ist, darf man sie beim Reaktivieren einer ID nicht weiter verwenden. Seit PHP 4.3.2 gibt es die Funktion session_regenerate_id(), die eine neue Session erzeugt. Aber wie erkennt man, dass die Session-ID evtl. unsicher ist?

Man könnte per session_id() eine eigene, sichere Session-ID setzen. Aber das krankt an zwei Problemen:
  • Es gibt in PHP keine Möglichkeit, um eine kryptologisch sichere Zufallszahl zu erzeugen. Dies ist zwar im Core eingebaut, aber man kann auf diesen Code nicht zugreifen. (Auch ein typisches Merkmal von PHP, dass es nämlich wichtige bis wichtigste Interna vom Programmierer fernhält, stattdessen werden lieber zillionen von Feature-Funktionen eingeführt, die nur Teilprobleme ansprechen, aber den Programmierer mehr entmündigen als zur Problemlösung befähigen.) Natürlich kann man mit krassem Aufwand all das nachrüsten, aber das macht den Code dann auch noch betriebssystemabhängig, da die dafür notwendigen Daten unter Windows woanders stehen als unter Unix usw.
  • Die Lösung wäre notwendigerweise inkompatibel zu jeder anderen Lösung (die Session-ID wäre sonst vorhersagbar). Man kann also zwei verschiedene Lösungen nicht parallel verwenden, da diese sich gegenseitig immer die Session-ID ersetzen.
Man muss also annehmen, dass die Session-ID grundsätzlich unsicher ist. Diese Annahme ist gar nicht mal so schlecht. So lange also die Session-ID gültig ist verwendet man sie, aber sobald sie ungültig wird (austimet), gibt man eine neue aus. Dies muss man außerdem von Zeit zu Zeit aus Sicherheitsgründen wiederholen.

Die Methode dies zu erreichen ist eigentlich ziemlich einfach. Man setzt eine Variable in der Session (dies ist dann die einzige) die das Enddatum der Session festlegt. Wird dieses Enddatum überschritten (oder ist nicht gesetzt) wird die Session regeneriert. Außerdem wird die Session bei jedem Login-Vorgang regeneriert.

Verwenden mehrere Webservices denselben Code sind sie automatisch kompatibel. Verwenden mehrere Webservice verschiedene dieser Lösungen (also verschiedene Variablen) so ist dies weiterhin kompatibel, die Session wird dann nur häufiger regeneriert.

Lösungen

  • Man verwendet session_name() nicht
  • Man verwendet ständig session_regenerate_id() um sicherzustellen, dass die Session kryptologisch sicher ist.
  • Man verwendet session_id() nicht, da sich diese ja laufend ändern muss.
  • Man verwendet session_set_save_handler() nicht, da dieses nicht bereitsteht, wenn ein anderer Web-Service parallel verwendet wird.
  • Man verwendet session_register() nicht, außer um interne Variablen des Session-Managements selbst zu speichern.
Das bedeutet, um ein brauchbares Session-Management zu bekommen muss man 90% der von PHP bereitgestellten Funktionalität auf den Müll werfen.

Das Session-Management selber verwendet eindeutige, konfigurierbare Variablen (Prefix) in der Session, um die Validität der Session zu überprüfen. Wird die Session regeneriert, kann die alte Session-ID übrigens gelöscht werden, da im Browser immer nur eine Session vorhanden sein kann. Durch setzen des Prefix wird das Session-Management auch kompatibel zu anderen Session-Managements, die nicht so vorsichtig vorgehen wie hier beschrieben.

Die Alternative wäre, das gesamte Session-Management auf einem eigenen Cookie basieren zu lassen. Das Problem dabei ist aber, einen kryptologisch sicheren Zufallswert zu erzeugen. Außerdem hätte man dadurch nichts gewonnen, sondern nur noch mehr Cookies erzeugt.

Die "Remember Me"-Funktion kann man über solch ein Zusatz-Langzeit-Cookie festlegen, aber ein solches sollte niemals für Sessions verwendet werden. Für die Browser-Session ist es also nicht sinnvoll, es noch weiter selber zu machen.

Sicheres Session-Management

Ich werde hier den Code veröffentlichen, sobald ich ein stabiles Session-Modul implementiert habe das all den hier beschriebenen Kriterien entspricht.

Eine wichtige Grundlage für ein Session-Management bei mir ist beispielsweise, dass ein Cookie nur dann gesetzt wird, wenn es notwendig wird. So lange ich also noch nicht eingeloggt bin (etc.), darf auch kein Cookie gesetzt werden. Das machen so ziemlich 99% aller Webanwendungen falsch, nämlich sie setzen auch dann ein Cookie, wenn es gar nicht gebraucht wird.

Der Nachteil ist, dass der Webserver dann eine Session verwalten muss, die überhaupt keinen Sinn ergibt. Außerdem werden unsinnig Session-IDs in der Welt verteilt, was vermutlich einige Terabyte an täglich unsinnig herumgeschickten Daten und mehrere tausend Kilowatt an unsinnig verbrauchter CPU-Leistung bedeutet. Die nicht notwendige ID muss ja kryptologisch sicher erzeugt werden und wird vom Browser gespeichert.

-Tino, 2008-06-01