Frost on a roof

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?