Archiv

Ende der Abzocke

Liebe junge Leute, um diesem Artikel von 2004 etwas Sinn zu geben: Zu der Zeit war es noch üblich, sich mit einem Modem ins Internet einzuwählen. Man wählte die Telefonnummer seines Providers und surfte dann zum Minutenpreis über die Telefonleitung. Bei der Betrugsmasche, um die es hier geht, stellte eine Malware - der so genannte Dialer - die Rufnummer heimlich auf eine teure Abzock-Nummer um. Die “Überraschung” kam dann zum Monatsende in Form einer Telefonrechnung in vierstelliger Höhe.

Heutzutage geht praktisch jeder per DSL, Kabel oder Mobilfunk ins Internet. Dort funktioniert die Masche schon technisch nicht.

Kaum etwas verkörpert die Abzocke im Internet besser als der so genannte Dialer. Selbst Gesetzesänderungen schienen ihm kaum etwas anhaben zu können. Doch jetzt hat das BGH Recht gesprochen.

Ein Dialer ist ein Programm, dass die Internet-Verbindungseinstellungen des Rechners auf einen anderen Anbieter umstellt; meist auf eine teure 0190- oder 0900-Nummer. Die wenigen seriösen Dialer, die die Verbindungskosten vorher deutlich bekannt geben, sind dabei von dem Urteil nicht betroffen. Es geht hier vielmehr um die “Abzock-Dialer”, welche das System unbemerkt umstellen. Wenn sie vorher überhaupt fragen, geben sie sich lediglich als “Optimierungstool für Internet-Verbindungen” aus, verschweigen die dabei anfallenden immensen Kosten jedoch. Wenn man die Frage, ob er sich installieren soll, verneint, installieren sie sich meist erst recht, und auch eine Deinstallation hilft selten wirklich. Andere Dialer installieren sich völlig verborgen. Abzock-Dialer durchseuchen das Netz zunehmend, man findet sie nicht mehr nur auf Pornoangeboten, sondern auf Seiten zur Währungsumrechnung oder mit Geburtstagsgedichten, in E-Mails und mittlerweile sogar auf Webseiten, die speziell für Kinder gemacht sind. Manche nutzen Sicherheitslücken des Windows-Betriebssystems, um sich unbemerkt aktivieren zu können.

Selbst ein kürzlich dazu erlassenes Gesetz, das eine Registrationspflicht für Dialer eingeführt hat und die Kosten begrenzt, konnte dem Treiben kaum einhalt gebieten. Wer sowieso Recht bricht, schert sich wenig um neue Gesetze.

Das Problem des betrogenen Kunden ist, dass der Betrüger selten auszumachen ist. Die 0190-Nummern werden von der RegTP im Block an Netzbetreiber vergeben, welche ihrerseits Teile des Blockes an Mehrwertdienstunternehmen vermieten. Oft sitzt hinter den betrügerischen Dialer-Unternehmen jedoch eine Briefkastenfirma im Ausland. Es fällt immens schwer, überhaupt die Adresse des Betrügers ausfindig zu machen, und wenn er im Ausland sitzt, hat man schließlich wenig Chancen, sein Geld zurück zu erhalten.

Der Telefonnetzbetreiber, der seinem betrogenen Kunden eigentlich helfen sollte, zuckt meist mit den Schultern, weil er die gleichen Probleme hat, den Betrüger ausfindig zu machen - aber verdient dennoch an dem ergaunerten Geld mit, da er einen Teil der 0190-Gebühren für Mehrwertdienste einbehält.

Die Forderung nach Gegenmaßnahmen verpufften. So recht schienen die Netzbetreiber kein Interesse zu haben, ihre Kunden zu schützen und den Betrugsmaschen ein für allemal einen Riegel vorzuschieben.

Das Urteil des BGH vom Freitag wird dies nun ändern. Das BGH hat nämlich festgestellt, dass der Netzbetreiber an den 0190-Nummern mitverdient, und deshalb auch ein Risiko mittragen muss. Für den Kunden heißt es, wenn er einen betrügerischen Dialer untergeschoben bekommen hat, braucht er die von ihm verursachten Gebühren nicht zu zahlen, sondern nur die Onlinegebühren, die er hätte bezahlen müssen, wenn der Dialer nicht installiert gewesen wäre. Der in diesem Fall beklagte Telefonnetzbetreiber blieb also auf den Gebühren von €9000 sitzen, die der Dialer beim Kunden verursacht hatte.

Zugegeben: der Netzbetreiber ist nicht Verursacher des Problems gewesen, er hat jetzt lediglich den schwarzen Peter zugeschoben bekommen. Allerdings hat der Netzbetreiber eher die Macht und die Möglichkeit, die Verträge mit den Mehrwertdiensteanbietern zu gestalten und zum Beispiel die Haftung für betrügerische Dialer weiterzureichen, bis es letztendlich den Betrüger selbst treffen würde. Damit würde dieser Sumpf der Abzock-Dialer endgültig ausgetrocknet werden.

Und das wird auch wirklich höchste Zeit!

Das BGH-Urteil trägt das Aktenzeichen III ZR 96/03.

Spam-Statistik

Meine offizielle Spam-Statistik für August 2003

Insgesamt erhielt ich im August 595 Spam-Mails und 4 Viren, das macht im Schnitt 19,3 Mails pro Tag. Wenn der Spam halten würde, was er verspricht, hätte ich am Monatsende folgendes zusammen gehabt:

Körper und Gesundheit

  • Diät: 45 Monatsrationen HGH umsonst. Damit hätte ich in fast 4 Jahren gut 1350 kg abnehmen können.
  • Penis: Verlängerung um 325 inches. Das entspricht einer Penislänge von über 8 Metern.
  • Viagra: 2 Flaschen und 5 Tabletten kostenlos.
  • Brustvergrößerung: 3 Flaschen umsonst. Das entspräche einer Vergrößerung um 9 Cups.
  • Rauchen: 2 mal Rauchen aufhören in zwei Wochen. Dabei bin ich Nichtraucher.

Geld

  • Nigeria-Connection: US $273.520.000,00 hätte ich als “vertrauenswürdiger Partner” anteilig bekommen.
  • Lottogewinne: US $7.000.000,00 habe ich im Lotto gewonnen, obwohl ich kein Lotto spiele.
  • Geldgewinne: US $30,00 habe ich einfach so gewonnen.
  • Versicherungen: € 8.000,00 pro Jahr hätte ich an Versicherungsprämien sparen können. Das ist mehr, als ich pro Jahr für Versicherungen ausgebe.

Partnerschaft

  • Videonachrichten: 9 mal habe ich eine Videonachricht erhalten, aber nie abgerufen. Wie gemein von mir!
  • Nette Nacht: 1 mal bedankte sich eine unbekannte Frau für die nette Nacht mit mir.
  • Kontaktanfrage: 2 Frauen wollten mit mir zwecks Partnerschaft in Kontakt treten.

Sonstiges

  • Diplome: 2 Diplome beliebiger Art hätte ich mir kaufen können.
  • Reisen: 12 Tage Bahamas und 9 Tage Orlando mit freiem Eintritt in Disney World habe ich geschenkt bekommen.

Mit anderen Worten: Ich würde nun mehrfacher Multimillionär sein und mich am Strand der Bahamas als Prof. Dr. mit athletischer Figur, einem 8 Meter langen Schwanz und Titten, die Dolly Buster erblassen lassen würde, mit einem Dutzend schöner Frauen amüsieren. Und da sag noch mal einer, Spam macht nicht glücklich… Nur das Viagra könnte ich nicht nehmen, da der Blutsturz mich umbringen würde. 😆

Parameterprüfung

Hier wird beschrieben, wie du durch Parameterprüfung deine Seite absichern kannst.

Traue grundsätzlich niemandem

Wenn man PHP-Seiten entwirft, kommt man zwangsläufig mit Parametern in Kontakt, die dem Script über den Aufruf übergeben werden. Diese Parameter sind ein ganz simpler Angriffspunkt für Hacker, da sie sich sehr leicht manipulieren lassen.

Grundsatzregel ist also: misstraue immer allen Daten, die vom Benutzer kommen! Neben den Parametern, die man in $_GET und $_POST findet, gehören auch die Cookies dazu, da sie beim Benutzer lokal abgelegt werden und von ihm manipuliert werden können. PHP fasst alle Parameter, die grundsätzlich suspekt sind, in dem superglobalen Array $_REQUEST zusammen. Allerdings lässt sich auch der HTTP-Request vom Browser manipulieren, so zum Beispiel der HTTP_REFERER.

Die einfachste Art der Manipulation ist, bei einem GET-Request einfach die Parameterzeile im Browser zu verändern. Eine URL wie

posting.php?id=13&admin=no

schreit geradezu danach, mit den Werten herumzuspielen. Ich könnte zum Beispiel die ID verändern und so vielleicht an Postings kommen, die ich eigentlich gar nicht lesen dürfte. Oder ich setze admin auf yes und schaue, ob ich dann unerlaubt Admin-Rechte erhalte.

Man darf sich also grundsätzlich nicht darauf verlassen, dass die übergebenen Parameter vernünftig oder gültig sind. Selbst Einschränkungen zum Beispiel über eine begrenzte Auswahl in Select-Boxen oder eine Formularprüfung per JavaScript lassen sich auf diese Weise hervorragend aushebeln. Ebenso lassen sich eigentlich nicht änderbare Werte in hidden-Feldern vom Angreifer jederzeit verändern.

Es hilft nicht wirklich, die Parameter in der URL durch Frames oder POST-Requests zu verschleiern. Auch wenn eine Manipulation dann erschwert wird, ist sie dennoch weiterhin möglich. Frames lassen sich umgehen, und POST-Requests ganz leicht durch ein selbstgebautes kleines HTML-Formular nachbilden.

Also noch einmal: misstraue immer allen Daten, die vom Benutzer kommen!

Sensible Daten in Sessions auslagern

Eine Abhilfe ist schon mal, besonders sensible Daten nicht per Parameter an das nächste Script zu übergeben, sondern über die Session. Da die Session-Daten selbst auf dem Server gespeichert werden, hat der Benutzer keine Möglichkeit, diese zu lesen oder zu manipulieren.

So gehören also zum Beispiel die User-ID des eingeloggten Users oder etwa Rechte-Flags in die Session. Also einfach alles, wodurch sich der Benutzer durch Änderung der Werte Rechte erschleichen könnte, die er eigentlich nicht hat. Aber auch SQL-Queries sollten niemals als Parameter übergeben werden, sondern immer über die Session.

Permanente Daten gehören in eine Datenbank

Sessions leben nur so lange, bis der Browser geschlossen wird. Daten, die länger gespeichert werden sollen, kann man beim Benutzer als Cookie ablegen. Aber auch Cookies sind manipulierbar und damit stets suspekt.

Sensible Daten wie Benutzerrechte dürfen nicht im Cookie abgelegt werden. Eine Möglichkeit wäre, die Daten statt dessen auf dem Server in einer Datenbank zu speichern und dann lediglich eine Benutzerkennung im Cookie abzulegen. Der Benutzer darf allerdings keine Möglichkeit haben, durch Manipulation am Cookie eine andere Benutzerkennung zu erraten. Lege also bitte keine User-ID im Cookie ab. Stattdessen kannst du per Zufallszahlengenerator einen zufälligen Text erzeugen lassen, der durch md5() gehashed und dann mit in die Datenbank abgelegt wird. Dieser Hash-Code kann im Cookie abgelegt werden, denn es ist praktisch ausgeschlossen, den Hash-Code eines anderen Benutzers zu erraten.

Die sicherste Methode, Benutzerdaten permanent auf dem Server zu halten, ist allerdings nach wie vor, Benutzerkonten zu führen und jeden Benutzer zu bitten, sich am Anfang mit einem Usernamen und einem eigenen Passwort in das System einzuloggen.

Parameterprüfung

Ein weiterer wichtiger Schritt ist, die an das Script übergebenen Parameter auf plausibilität zu prüfen, also zu kontrollieren, ob der Wert auch sinnvoll ist. Diese Prüfung sollte ganz am Anfang erfolgen, noch bevor andere Programmteile auf die Parameter zugreifen. Ist ein Wert offensichtlich manipuliert worden, sollte das Script sich sofort beenden (zum Beispiel mit die()), um eine fehlerhafte Verarbeitung durch den falschen Wert zuverlässig zu verhindern. Da dieser Fehler nur durch einen gezielten Angriffsversuch ausgelöst werden kann, kann auf eine schmucke Fehlerseite verzichtet werden.

Ein Wert wurde zum Beispiel offensichtlich manipuliert, wenn man eine Selectbox im Formular hatte und nun einen Wert bekommt, der dort gar nicht zur Auswahl stand. Oder wenn man eine Datenbank-ID erhält, die es gar nicht in der Datenbank gibt.

Es liegt allerdings kein Manipulationsversuch vor, wenn der Benutzer schlicht und einfach eine falsche Eingabe gemacht hat. Wenn du also zum Beispiel ein einfaches Texteingabefeld für eine Jahreszahl hast, und du dort eine vierstellige Jahreszahl erwartest, der Benutzer aber nur “90” (für 1990) eingibt, ist das natürlich kein bösartiger Angriff, sondern nur eine Fehleingabe. Du solltest den Benutzer dann mit einer sprechenden Fehlermeldung höflich darauf hinweisen, was er falsch eingegeben hat und wie er eine richtige Eingabe tätigt, statt gleich das ganze Script abzubrechen und die Alarmglocken schrillen zu lassen.

Hilfreich für den Benutzer ist auch eine Formularprüfung per JavaScript, noch bevor die Daten zum Server geschickt werden. Das Formular sollte allerdings auch ohne JavaScript abzuschicken sein, und es sollte immer auch eine zusätzliche Überprüfung auf dem Server stattfinden! Ein Angriff lässt sich niemals durch geschickte JavaScript-Programmierung verhindern.

Ein Beispiel

Eine einfache Möglichkeit der Parameterprüfung ist, alle Parameter, die man erwartet, durch Prüffunktionen zunächst prüfen zu lassen. Diese Funktionen prüfen zumindest elementar die übergebenen Werte und kopieren sie im Erfolgsfall in ein eigenes, sichereres Parameter-Array um. Diese Funktionen können sich gleichzeitig um die Entfernung von Magic-Quotes kümmern.

Die grundlegendste Prüffunktion ist parText(). Sie erwartet den Namen des Parameters und optional einen Defaultwert, wenn der Parameter nicht übergeben wurde. Er liefert true zurück, wenn der Parameter übergeben wurde, sonst false.

function parText($name, $default=null) {
  global $PAR;
  $PAR[$name] = $default;
  if(isset($_REQUEST[$name])) {
    $val = $_REQUEST[$name];
    if(get_magic_quotes_gpc()) $val = stripslashes($val);
    $PAR[$name] = $val;
    return true;
  }
  return false;
}

Ein paar Beispiele, wie man so eine Parameterprüfung aufrufen kann:

require_once('parameter.php');      // die Funktion einbinden

// 1: Es gibt einen optionalen Parameter namens 'foo'.
parText('foo');

// 2: Es gibt einen optionalen Parameter 'foo'. Gibt es ihn nicht,
//    wird 'Hallo' als Defaultwert angenommen.
parText('foo', 'Hallo');

// 3: Parameter 'foo' ist ein Pflichtparameter. Gibt es ihn nicht,
//    liegt eine Manipulation vor.
if(!parText('foo')) die("Parameter 'foo' fehlt!");

// Der Wert von 'foo' liegt anschließend im Array $PAR
if(isset($PAR['foo'])) {
  print("foo ist: ".htmlspecialchars($PAR['foo']));
}else {
  print("foo ist nicht übergeben worden.");
}

Das Array $PAR ist natürlich keine superglobale Variable und muss in Funktionen weiterhin mit global $PAR eingebunden werden. Durch diese Trennung weiß man jedoch, dass die Werte in $PAR zumindest eine einfache Prüfung durchlaufen haben und dadurch ein wenig vertrauenswürdiger sind als die Werte in $_REQUEST.

Analog dazu kann man jetzt weitere Prüffunktionen schreiben. Für die meisten Eingabefelder a la input type="text" reicht es zum Beispiel, den Text auf 256 Zeichen zu begrenzen. Da die Eingabe nur eine Textzeile zulässt, können außerdem alle Zeilenumbruch-Steuerzeichen herausgefiltert werden. In Code gegossen sieht das dann so aus:

function parPlain($name, $default=null) {
  global $PAR;
  $PAR[$name] = $default;
  if(isset($_REQUEST[$name])) {
    $val = trim($_REQUEST[$name]);
    if(get_magic_quotes_gpc()) $val = stripslashes($val);
    if(strlen($val)>256) $val = substr($val,0,256);
    $PAR[$name] = preg_replace('/(\n|\r)*/', '', $val);
    return true;
  }
  return false;
}

Erwarte ich zum Beispiel eine natürliche Zahl als Eingabe, kann ich folgende Funktion verwenden:

function parInt($name, $default=null) {
  global $PAR;
  $PAR[$name] = $default;
  if(isset($_REQUEST[$name])) {
    $val = trim($_REQUEST[$name]);
    if(get_magic_quotes_gpc()) $val = stripslashes($val);
    $PAR[$name] = intval($val);
    return true;
  }
  return false;
}

Nachdem überflüssige Leerzeichen abgeschnitten wurden, wird mittels der Funktion intval() anschließend sicher gestellt, dass sich im umgesetzten Parameter in $PAR garantiert eine Zahl befindet. Im Anschluss könnte man noch prüfen, ob die Zahl innerhalb eines Gültigkeitsbereichs liegt.

Die Vorprüfung für ein Formular mit User-ID, Vor- und Nachname sowie einem optionalen Freitext sähe dann zum Beispiel so aus:

require_once('parameter.php');      // die Funktion einbinden

// User-ID (Pflichtparameter)
if(!parInt('id'))         die("Parameter 'id' fehlt!");

// Vor- und Nachname (Pflichtparameter)
if(!parPlain('vorname'))  die("Parameter 'vorname' fehlt!");
if(!parPlain('nachname')) die("Parameter 'nachname' fehlt!");

// Ein mehrzeiliger Freitext (optional)
parText('freitext');

Dies ist allerdings nur eine sehr grundlegende Prüfung. Es sollten nun weitere Prüfungen folgen, zum Beispiel ob die User-ID gültig ist.

Behandlung von Passwörtern

Wenn du in einem Formular ein Eingabefeld für Passwörter verwendet, solltest du stets das input type="password"-Eingabefeld verwenden. Dies schützt das Passwort vor neugierigen Blicken auf den Bildschirm.

Außerdem sollten Formulare mit Passwörtern stets ausschließlich per POST an den Server geschickt werden. So erscheint das Passwort niemals in der URL und wird zum Beispiel auch nicht in Proxies zwischengespeichert.

Entsprechend solltest du im Script das Passwort nur von dem $_POST-Array entgegennehmen, nicht jedoch von $_GET oder von $_REQUEST.

Dateiuploads

Dateiuploads lassen sich bei PHP über ein Hidden-Feld in dem Formular in der Größe beschränken. Aber auch dieses Feld ist manipulierbar. Du solltest also unbedingt auch noch im Script prüfen, ob die empfangene Datei das Größenlimit überschritten hat, und gegebenfalls die Ausführung abbrechen.

Mittlerweile sollte sich auch herumgesprochen haben, dass man für Dateiuploads stets das $_FILES-Array oder die is_uploaded_file()-Funktion zur Prüfung verwenden sollte. Gerade der Dateiupload bietet eine relativ einfache Möglichkeit für einen Angreifer, Dateien und Passwörter vom Server auszuspähen, wenn man hier nicht sorgfältig genug vorgeht.

Code-Injections

Größte Vorsicht ist geboten, wenn man Parameter vom Server als Programmteil ausführen lässt, wenn sie also ganz oder teilweise an system(), eval(), mysql_query() oder entsprechend andere Funktionen übergeben werden. Du gibst damit einem Angreifer die Chance, Daten auszuspähen oder massiven Schaden anzurichten.

Bei Funktionen wie include() oder require() erlaubt es PHP normalerweise, auch externe Quellen (per “http://”, “ftp://” etc) anzugeben. Wenn ein Parameter ungeprüft an include() übergeben wird, könnte ein Angreifer so sehr bequem ein beliebiges externes Script einbinden.

Solche Parameter lassen sich nur schwer auf bösartigen Code überprüfen, du solltest es also meiden wie der Teufel das Weihwasser! Wenn es sich absolut nicht vermeiden lässt, solltest du entsprechende Vorsichtsmaßnahmen ergreifen und darauf achten, dass sich diese Maßnahmen nicht aushebeln lassen. Siehe dazu auch den Artikel über SQL-Injections.

Zusammenfassung

Um dich gegen Angriffe durch manipulierte Parameter zu schützen, solltest du:

  • grundsätzlich allen Daten misstrauen, die vom User kommen (GET-Parameter, POST-Parameter, Cookies, HTTP-Header)!
  • sensitive Daten niemals per Request, sondern per Session an andere Scripte weitergeben.
  • alle Parameter vor ihrer Benutzung auf Plausibilität prüfen.
  • niemals Parameter an system(), eval(), mysql_query() oder andere Funktionen der Art übergeben, ohne sie gründlich zu prüfen und vor dem Aushebeln der Prüfmechanismen zu schützen.
  • Passwörter stets mit POST übertragen und nur vom $_POST-Array entgegennehmen.

Plausibilitätsprüfungen schließen ein:

  • Sind alle Pflichtparameter vorhanden?
  • Ist ein erwarteter numerischer Wert (Datenbank-ID) wirklich numerisch?
  • Existiert ein Datenbankeintrag mit der übergebenen ID?
  • Enthält ein einzeiliges Eingabefeld unerlaubte Zeichen (z.B. Zeilenumbrüche)?
  • Wurde von einer Selectbox ein Wert übermittelt, der gar nicht in der Selectbox stand?
  • Wurden Mehrfachparameter übergeben, obwohl nur eine Einfachauswahl erlaubt ist? Wurden beispielsweise mehrere Radio-Buttons einer Gruppe ausgewählt, oder mehrere Einträge einer Selectbox mit Einfachauswahl?
  • Wurde die Datei tatsächlich hochgeladen und übersteigt sie nicht die erlaubte Maximalgröße?
  • Hat der Benutzer tatsächlich das Recht, die Aktion auszuführen oder den Datenbankeintrag zu sehen?
Session schützen

Du findest hier, wie man Session davor schützen kann, gekapert zu werden.

Wie funktionieren Sessions?

PHP erlaubt es mit den Sessions, Daten für den Nutzer über die gesamte Sitzung (engl. Session) hinweg vorzuhalten und für die Scripte verfügbar zu machen. So ließe sich zum Beispiel ein Warenkorb ohne Datenbank alleine über die Sessions verwalten, indem die gewünschten Waren in der Session des Nutzers eingetragen werden.

Damit PHP die Session einem bestimmten Nutzer zuordnen kann, vergibt es jeder neuen Session eine sogenannte Session-ID. Diese recht lange Zahl muss jedem Script übergeben werden, damit es die zugehörige Session finden kann. Die Session-ID wird entweder in einem temporären Cookie abgelegt, welches beim Schließen des Browsers gelöscht wird, oder aber als Parameter von Script zu Script weitergegeben. Geht die Session-ID verloren, gibt es keine Möglichkeit mehr, an die dazugehörige Session heranzukommen.

PHP räumt gelegentlich die Sessions auf und löscht die Session-Daten, auf die seit einer bestimmten Zeit nicht mehr zugegriffen wurden. Eine Session taugt also nicht dazu, benutzerabhängige Daten längerfristig auf dem Server zu speichern.

Was bedeutet nun “Session kapern”?

Die Daten der Session selbst liegen auf dem Server und sind vor Missbrauch geschützt. Der Ansatzpunkt für das Kapern ist allerdings die Session-ID. Eine Session wurde gekapert, wenn es einem unberechtigten User gelingt, an diese Session-ID zu kommen und sie für sich zu verwenden. Für PHP ist er dann von dem eigentlichen User nicht mehr zu unterscheiden. Wenn der User besondere Rechte (zum Beispiel Admin-Rechte) hat, hat sie der Angreifer durch das Kapern der Session-ID ebenfalls.

Es gilt nun, dieses Kapern der Session zu erschweren oder praktisch ganz zu verhindern.

Datenlecks

PHP erzeugt die Session-ID automatisch, und verwürfelt die Zahl dabei so gründlich, dass es praktisch unmöglich ist, eine Session-ID einfach zu erraten oder von einer bekannten ID auf eine unbekannte zu schließen. Auch sonst ist es für einen Angreifer nicht ohne weiteres möglich, eine beliebige Session zu kapern. Er muss dazu schon bestimmte Voraussetzungen erfüllen. Im Allgemeinen sind Sessions also ausreichend sicher.

Dennoch gibt es zum Teil sehr einfache Angriffspunkte, eine fremde Session-ID auszuspähen.

Der use_trans_sid-Modus hängt zum Beispiel die Session-ID automatisch an jede URL, die auf den eigenen Server verweist. Das wird schnell zum Problem, da die Session-ID nun grundsätzlich an die URL angefügt wird. So könnte ein User eine aktive Session per Mail oder Instant Messenger weiterverschicken, wodurch der Empfänger mehr oder weniger unfreiwillig die Session gekapert hätte. Dieser Modus gehört also grundsätzlich abgeschaltet!

Eine weitere große Session-Tratsche ist der Browser des Users! Er überträgt in der Regel bei jedem Request, den er absetzt, im Referer-Feld des HTTP-Headers die URL der Seite, von der der Aufruf ausging. Und diese URL kann natürlich auch die Session-ID enthalten. In der Praxis bedeutet es, dass die Session-ID zum Beispiel an die Server von Werbebannern weitergegeben wird. Und auch wenn der User einem externen Link folgt, wird die Session-ID möglicherweise im Referer-Feld mit an den Server geschickt.

Ein mögliches Angriffsszenario wäre ein PHP-Forum, das es seinen Usern erlaubt, einen externen Link auf ein Avatar-Bild1 von sich anzugeben. Wenn nun ein anderer User auf eine Forums-Seite kommt, die das Avatar-Bild des Angreifers enthält, greift dessen Browser auf das externe Bild zu und übermittelt dem Angreifer die Session-ID des Users in dem Referer-Feld. Jener braucht nur noch sein Logfile auszuwerten, um die gekaperten Session-IDs zu “ernten”. Generell reicht es, wenn der Angreifer auf einer Webseite einen externen Link auf seinen Server setzen kann, und schon kommen die Session-IDs für ihn frei Haus.

Eines darf man dabei allerdings nicht vergessen: den Sessions ist ein relativ kurzes Leben beschert. Bereits etwa eine halbe Stunde nach dem letzten Zugriff werden sie von PHP für gewöhnlich geschlossen. Eine gekaperte Session-ID bringt dem Angreifer also nur dann etwas, wenn sie noch frisch ist.

Abwehr

Wie kann man sich nun davor schützen? Der beste Schutz greift eigentlich beim Surfer. Ironischerweise ist hier die sicherste Methode gleichzeitig die mit dem schlechtesten Ruf: er sollte Cookies akzeptieren.

Im Internet kursieren die merkwürdigsten Gerüchte, warum Cookies unsicher sind und ausgeschaltet werden müssen. Angeblich soll es mit Cookies sogar möglich sein, die E-Mailadresse des Surfers zu ermitteln oder ihm einen Virus unterzujubeln. Das ist hausgemachter Blödsinn! Wahr ist jedoch, dass man mit Cookies das Surfverhalten eines Surfers in einem bestimmten Rahmen analysieren kann.

Was das Session-Kapern angeht, ist der Surfer, der Cookies erlaubt, allerdings stets auf der sicheren Seite. Denn dann legt PHP die Session-ID im Cookie ab, und an die kommt ein Angreifer dann höchstens noch, wenn er den Datenverkehr zwischen Browser und Server gezielt abhört. Dies ist jedoch eine relativ hohe technische Hürde, und selbst dagegen kann man sich mit SSL schützen.

Alternativ kann der Surfer dafür sorgen, dass der Referer nicht mehr im HTTP-Header übertragen wird. Einige Browser bieten die Möglichkeit, die Übertragung abzuschalten oder zu verfälschen. Es gehört aber auch zum Standardrepertoire verschiedener Webwasher und Proxies. Leider haben einige Webseiten, die sich auf das Referer-Feld verlassen (zum Beispiel um Deep Linking zu verhindern), damit erhebliche Probleme.

Washer-Seiten

Als Programmierer kann man das Problem über sogenannte Washer-Seiten lösen. Anstatt externe Links direkt anzuspringen, stellt man diese Washer-Seite dazwischen. Sie wird stets ohne Session-ID in der URL aufgerufen (use_trans_sid muss dafür abgeschaltet sein) und leitet den Request dann direkt an die externe Seite weiter.

Dadurch findet der externe Server im Referer-Feld nicht mehr eine Seite mit Session-ID, sondern nur noch die URL der Washer-Seite. Und diese wurde ja ohne Session-ID aufgerufen.

Kleiner Haken

Einen kleinen Haken hat die Geschichte allerdings noch. Selbst wenn Cookies erlaubt sind, wird die Session-ID von dem ersten Request aus in der URL übertragen.

Der Grund ist: es gibt keine Möglichkeit herauszufinden, ob der Browser Cookies akzeptiert oder nicht. Man versucht einfach sein Glück, setzt das Cookie, und wenn es beim nächsten Request zurückkommt, hat es wohl geklappt. Bei der ersten Seite, die aufgerufen wird, ist es jedoch noch ungewiss, und PHP muss deshalb beide Wege gehen. Nach der Erzeugung der neuen Session wird die ID in einem Cookie abgelegt und gleichzeitig an die URLs angehängt. Nur so ist gewährleistet, dass die Session-ID an die nächste Seite weitergegeben wird, auch wenn Cookies nicht erlaubt waren. Die zweite Seite, die der User ansurft, wird also mit der Session-ID in der URL aufgerufen und dementsprechend wieder vom Browser im Referer weitergegeben.

Es ist nicht möglich, den Effekt vollkommen auszuschließen. Aber zumindest konnten wir die Wahrscheinlichkeit, dass die Session-ID gekapert werden kann, auf nur eine Seite reduzieren.

Seit PHP 4.3.32 bietet PHP nun die Möglichkeit, mit der Funktion session_regenerate_id() die in der URL veröffentlichte Session-ID nachträglich zu ändern. Wenn man also feststellt, dass die Session-ID als Cookie und in der URL übertragen wurde, kann man diese Funktion aufrufen, um anschließend im Cookie eine neue Session-ID zu speichern. Die ID, die ein Angreifer von der URL abgefangen haben könnte, ist dann wertlos.

Nummer Sicher

Um sich gegen einen Session-Klau zu schützen, empfiehlt sich eine Kombination aus folgenden Techniken:

  • Die Verwendung von Cookies zum Ablegen der Session-ID wird erzwungen. Dies geht, indem man use_trans_sid abschaltet, use_only_cookies aktiviert und selbst nirgendwo die Session-ID an eine URL anhängt. Allerdings muss hier der User mitspielen, wovon man eigentlich nur in Intranets ausgehen kann.
  • Es wird SSL verwendet, um ein Mitschneiden der Cookie-Daten zu verhindern.
  • Wenn eine HTTP-Authentifizierung verwendet wird, können die Authentifizierungsdaten mit in der Session gespeichert werden. Der Request ist nur dann gültig, wenn die Authentifizierungs-Kredentien vom Browser übertragen werden und mit den Daten in der Session übereinstimmen.
  • Eine in der URL veröffentlichte Session-ID wird mit session_regenerate_id() sofort invalidiert, sofern Cookies erlaubt sind.

Was man dagegen nicht (oder höchstens in einem Intranet) machen sollte, ist die Session-ID mit der IP-Adresse des Users zu verknüpfen. Was sich zunächst wie eine gute Idee anhört, geht in der Praxis böse nach Hinten los. Denn spätestens dann, wenn kaskadierte Proxies eingesetzt werden, kann jeder Request von einer anderen IP-Adresse aus kommen. Bei dem T-Online-Proxy ist das zum Beispiel der Fall. Hier würde dann auch der berechtige User wegen angeblichem Session-Klau abgewiesen werden, weil er plötzlich eine andere IP verwendet.

Zusammenfassung

User können sich davor schützen, dass die Session gekapert wird, indem sie:

  • zumindest temporäre Cookies erlauben, so dass die Session-ID im Cookie abgelegt wird.
  • sich vor dem Verlassen einer Site stets von ihr ausloggen, sofern es eine Möglichkeit dafür gibt.
  • eine Möglichkeit finden, das Referer-Feld zu unterdrücken oder zu verfälschen.

Entwickler können ihre User weitgehend davor schützen, indem sie:

  • ihre User ermutigen, Cookies für die Site zuzulassen. Wenn du erklärst, wozu du das Cookie brauchst, dürfte es einfach sein, deine User zu überzeugen.
  • Washer-Seiten vor externe Links schalten, die die Session-ID aus dem Referer-Feld entfernen.
  • in der URL veröffentlichte Session-IDs mit session_regenerate_id() umgehend invalidieren, wenn der User Cookies zulässt.

Nicht dagegen sollte die Session mit der IP-Adresse des Users verknüpft werden.


  1. Ein Avatar ist ein Abbild von dem jeweiligen User in der virtuellen Welt des Internet. Es kann ein Foto von dem User sein, aber oft ist es auch eine Comiczeichnung oder ein Symbol. ↩︎

  2. Eigentlich gibt es die Funktion bereits seit PHP 4.3.2, dort ändert sie aber den Cookie nicht ab und ist damit für unsere Zwecke wertlos. ↩︎

Erste Woche ermitteln

Dieses Script ermittelt den Timestamp der ersten Woche eines Jahres.

Die Wochen eines Jahres werden durchnummeriert. Das ist leider ein wenig komplizierter, als es auf den ersten Blick aussehen mag, da die erste Woche nicht mit dem 1. Januar beginnt.

Nach ISO 8601 ist die erste Woche des Jahres die Woche mit dem 4. Januar.

Das folgende Script ermittelt nun zu einem Jahr den Timestamp von Mitternacht des Montags der ersten Woche.

/** 
 * Get a timestamp of midnight of the first week's Monday of the given
 * year.
 *
 * @param   $year     Year to compute the timestamp for (4 digits)
 * @return  Timestamp of the first week's Monday
 */
function computeFirstWeek($year) {
  // Get the timestamp of January 4th of this year
  $jan4 = mktime(0,0,0,1,4,$year);

  // Get the weekday of that day (with Monday being 0)
  $wd = (date('w',$jan4) + 6) % 7;

  // Go back those number of days, to reach Monday
  $jan4 -= $wd*24*60*60;

  return $jan4;
}