Checkliste für Websicherheit

Checkliste für Websicherheit

Inhalt

  1. XSS (Cross Site Scripting)
  2. SQL-Injection
  3. Benutzereingaben
  4. CSRF (Cross Site Request Forgerey)
  5. Dateiuploads
  6. Komplexe Berechnugen
  7. Geheist werden
  8. Header Injection
  9. Session Fixation
  10. Security through Obscurity
  11. Umgang mit Passwörtern

Um unseren kleinen Beitrag zu einer sicheren Welt zu leisten wollen wir nicht nur zum Welt AIDS Tag Kondome über Outdoor-Kunstwerke an Bahnhöfen stülpen, sondern diesmal auch etwas eher praktisch Orientiertes anbieten. Im Folgenden sehen wir uns eine Liste möglicher Angriffe auf Webanwendungen an. Es geht dabei nicht zwangsweise um PHP, auch wenn die Beispiele aufgrund unserer Firmennatur PHP-Code sein werden. Außerdem geben wir uns nicht mit einer popeligen Gliederung ab, da die meisten Probleme eh vielfältig sind und Schubladen nicht mögen.

Ich werde wenig bis gar nicht auf praktische Angriffe eingehen, da ich davon ausgehe, dass jeder entweder Googlen bemühen kann oder bereits weiß, wie XSS & Co. funktionieren.

XSS (Cross Site Scripting)

Werden alle Ausgaben, die in irgendeiner Form vom Benutzer abhängen, entsprechend dem Ausgabemedium kodiert?

Das heißt, werden bei HTML alle Variablen durch htmlspecialchars() verarbeitet? Dabei nicht vergessen: Encoding richtig setzen! Andernfalls knallt's bei Multibyte-Ausgaben ganz schnell.

(Wir gehen davon aus, dass in page vermutlich die aktuelle Seite einer Auflistung enthalten ist. Und, dass dieser Wert nicht negativ sein sollte.

Benutzerspezifische Werte können aus vielen Quellen kommen:

Hier gilt: Immer so spät wie möglich escapen. HTML-kodierte Werte bringen in der normalen Applikation rein gar nichts. Auf keinen Fall sollte htmlspecialchars() irgendwo im Model auftauchen, in der Regel auch nicht im Controller.

Spätes Escapen ergibt nicht nur mehr Sinn, sondern sorgt auch dafür, dass das Ausgabemedium beliebig gewechselt werden kann. Statt HTML könnte LaTeX die Ausgabe sein, bei der dann natürlich kein htmlspecialchars() mehr zum Einsatz kommen muss (dafür dann eine texspecialchars()).

SQL-Injection

(Ich komme zu dem Thema, weil sich hier die logische Fortsetzung der Ausgabekodierung gut anschließen lässt.)

Werden alle Werte, die in irgendeiner Form in Queries verwendet werden, entsprechend dem DBMS und ihrem Datentyp kodiert?

Das alte Problem von SQL-Injections dürfte so ziemlich jedem bekannt sein:

$qry = "SELECT * FROM mytable WHERE key = $_GET[foo]";

Hier müssten wir

Gerade das Escapen kann jedoch tricky sein: Wie schon beim Escapen der Ausgabe muss hier der Zeichensatz beachtet werden. addslashes() kennt die verschiedenen SQL-Engines nicht und wird daher die spezifischen Steuerzeichen ebenso unbeachtet lassen wie Multibyte-Strings. Im Falle von MySQL ist also mysql_real_escape_string() die Funktion der Wahl.

Doch auch hier gilt Vorsicht: Ändert man den Zeichensatz der Verbindung nur über MySQL (mysql_query("SET NAMES 'utf8'")), so kriegt mysql_real_escape_string() von dieser Änderung nichts mit. Hier müsste man also stattdessen die native mysql_set_charset()-Funktion bemühen, sonst kann es bei UTF-8-Verbindungen auch zu Problemen kommen.

Im Idealfall abstrahiert man den Datenbank-Zugriff bei größeren Projekten gänzlich über einen OR-Mapper (Doctrine, Propel, …) weg oder nutzt für kleinere Projekte zumindest PDO und die dort verügbaren Prepared Statements, womit das lästige Escapen gänzlich unter den Tisch fällt.

Ein abschließender Hinweis dazu noch: Ein eventuell von PHP durchgeführtes Magic Quoting sollte in jedem Fall von der Anwendung erkannt und entfernt werden. Einerseits machen escapte Werte beim normalen Verarbeiten (ohne SQL) nur Probleme. Andererseits sind die Werte nicht richtig escaped, um in SQL verwendet zu werden (siehe Multibyte-Encodings). Die Anwendung sollte das Escaping immer so spät wie möglich vornehmen, bestenfalls beim Zusammensetzen der Anfrage. Auf diese Weise vermeidet man, dass manche Variablen escaped vorliegen und manche nicht. Das Escapen ist damit meistens Aufgabe des Models (in MVC-Architekturen).

Benutzereingaben

All users are either stupid, evil or both. And so is their input.

Das alte Spiel noch einmal zusammengefasst:

CSRF (Cross Site Request Forgerey)

(Eigentlich sollte man das in Konsistenz zu XSS auch mit XSRF abkürzen.)

Werden alle schreibenden Aktionen (Löschen, Anlegen, Verschieben, Umbennnen) per POST-Anfrage ausgeführt?

In einer idealen Welt würden wir wohl neben POST auch noch PUT und DELETE als HTTP-Methoden nutzen. Aber die Welt ist nicht ideal. Und POST reicht meistens auch vollkommen aus.

Die Wichtigkeit, schreibende Aktionen nur über POST zugänglich zu machen, zeigt sich, wenn man CSRF-Attacken näher untersucht. Ein Großteil der Angriffe wären nicht möglich gewesen, hätte die Anwendung auf POST gesetzt.

Dazu reicht es nicht, nur das eigene Formular auf POST zu ändern. Auch die Anwendung muss in derartigen Fällen explizit über $_SERVER['REQUEST_METHOD'] prüfen, ob es sich um POST handelt. Schöner wäre es noch, wenn man ein explizites CSRF-Token einführt, was aber in den meisten Fällen die Benutzerfreundlichkeit gewaltig drücken kann. Beispielcode könnte wie folgt aussehen:

function ensure_method($method)
{
   if (strtoupper($method) !== $_SERVER['REQUEST_METHOD']) {
      die('Request must be made using '.strtoupper($method));
   }
}

ensure_method('POST');

Dateiuploads

Sind Dateiuploads in Größe und Anzahl beschränkt?

Dateiuploads machen mir persönlich immer wieder Bauchschmerzen:

Irgendwie wirken Dateiuploads wie große, klaffende Wunden in der Anwendung. Jeder Besucher sieht sie, jeder kann etwas reintun und wenn's schmutzig war, stirbt die Anwendung. Kein schöner Zustand und auch ein wenig paradox, wenn wir alles andere mit Firewalls abriegeln.

Wichtige Leitsätze sollten hierbei sein:

Wenn viele (wirklich viele) Metadaten zu einer Datei benötigt werden und diese auch relativ klein sind, kann man auch darüber nachdenken, sie in der Datenbank (als BLOB) abzulegen. Bilder und größere Dateien würde ich davon jedoch immer ausnehmen, da man mit BLOBs und CLOBs sämtliche Stärken von SQL (Suchen, Selektieren, Verbinden, …) wegwirft (dann kann man auch CouchDB oder das Dateisystem nehmen).

Ein Beispiel soll noch verdeutlichen, wie gefährlich es sein kann, auch vermeintlich sichere Dateien einzubinden. Nehmen wir an, ein Benutzer lädt ein beliebiges Bild auf den Server. Mittels ImageMagick oder GD oder GIMP haben wir sichergestellt (indem wir versucht haben, es zu öffnen, was uns gelang), dass es sich wirklich um ein JPEG-Bild handelt. Theoretisch können wir es also einbinden.

// send.php
// Forbid direct access to a file and instead send it via this script so that
// we can add caching header and stuff.
include("$_GET[file].jpg");

Ganz davon abgesehen, dass dieser Code ohnehin mehrere Schwachstellen aufweist, nehmen wir ihn mal hin. Der Besucher sieht nun die folgende Ausgabe:

Warning: Unterminated comment starting line 1 in D:\test.jpg on line 1
 Ï Ó ►JFIF ☺☺☺ ` `   ■ %Hello from JPEG!

Was ist passiert? Nun, ein Blick in das Bild offenbart das Problem:

Hex-Ansicht des Bildes

Ich habe vor dem Upload als JPEG-Kommentar die Programmzeile

<?php print "Hello from JPEG!";/*

in mein Bild eingebettet. Und dabei war ich noch nett. Ich hätte auch den Webspace leeren oder dergleichen machen können. Und das auch ohne das Anzeigen von Fehlern. Wir lernen also, dass auch eigentlich geprüfte und für gut befundene Daten ihre Tücken haben können (und dass das Einbinden von hochgeladenen Dateien eine wirklich ganz schlechte Idee ist).

Komplexe Berechnugen

Kann mein Besucher über den Aufruf einer URL eine rechenintensive Aktion anstoßen?

Nehmen wir mal an, wir bieten einen Remote-Download-Service an. Besucher geben uns URLs, die sie aus irgendeinem Grund nicht aufrufen können, und wir laden sie für sie auf unseren Server (dabei gilt das eben schon für Dateiuploads Gesagte was identisch) und sie können sie später herunterladen. Wenn wir diesen Upload direkt auf Benutzerwunsch durchführen, ist nach 5 Besuchern (oder einem sehr bösen Besucher) unsere Bandbreite dicht. Das Gleiche gilt für komplexe Datenbank-Queries oder sonstige Berechnungen. Cachen oder Ablehnen. Wir wollen auch nicht, dass unsere Besucher mit jedem Seitenaufruf das Abrufen externer Quellen anstoßen können (z.B. soll ein Kunde in einem Shop nicht durch einen Klick auf einen Button eine Tracking-Anfrage an UPS starten dürfen).

Im Allgemeinen ist es hier sinnvoll, derartige Aufgaben vom Benutzer abzukoppeln. Zu holende Dateien (Remote-Download-Beispiel) können in einer Warteschlange landen. UPS-Trackings können per Cronjob unabhängig vom Geschehen auf der Webseite durchgeführt werden.

Im Idealfall kann ein Besucher durch seine Aktionen keine aufwändigen Operationen auf dem Server anstoßen.

Geheist werden

Läuft meine Anwendung auch noch dann [performant], wenn ich geheist / geslashdottet / populär werde?

Hier spielt die Sicherheit nur eine untergeordnete Rolle. Dennoch möchte ich das Thema gern ansprechen, da viele Seiten das Problem ignorieren. Beim Entwickeln der Anwendung sollte immer die Zielgruppe im Auge behalten werden: Facebook wird sicherlich keine Gesamtliste aller Mitglieder im Backend erzeugen während der kleine Tante Emma Laden um die Ecke seine Webseite sicher nicht redundant über 6 Server splitten wird. Augenmaß ist das Stichwort.

Einige Probleme, die auftreten können, wenn die Webseite und die in ihr enthaltenen Datenberge wachsen:

Nicht alle Probleme treffen auf alle Projekte zu, aber bei der Entwicklung sollte man derartige Probleme immer im Hinterkopf haben. Und sicherlich sind nicht alle Techniken immer bis ins Unendliche skalierbar und eine Neu-Implementierung wird notwendig.

Header Injection

Verwende ich Benutzerdaten in HTTP/eMail-Headern?

Das Problem mit HTTP-Headern ist nicht so dramatisch, da neuere Versionen von PHP in header() von Haus aus keine Zeilenumbrüche zulassen. Theoretisch kann aber eine böse Benutzereingabe dazu führen, dass in header() mehrere HTTP-Header erzeugt werden, was alle Arten von seltsamen Verhalten (Stichwort: Response Splitting) zur Folge haben kann. Hier gilt es, den Raum erlaubter Werte minimal zu halten ([a-z0-9.-]+ oder dergleichen).

Viel dramatischer sind hingegen Header in eMails. Ein typisches Script sieht wie folgt aus:

// Tell-a-friend-Script

$to = $_POST['to'];
$from = $_POST['from'];
$subject = "Hey, check this out!";
$body = "Hey, check out this wunderful website over there!";

mail($to, $subject, $body, "From: $from", "-f$from");

Ganz davon abgesehen, dass das Weiterreichen der Parameter an sendmail ohne weitere Prüfung schon fast einer Shell Injection gleicht, ist das Setzen des From-Headers sehr dramatisch. Nicht selten heißen die Empfänger solcher eMails plötlich

Timmy <timmy@foo.com>\nTo: spamopfer1@isp.com\nTo: foo@bar.com\nBCC: Foo\nSubject: Viagra!

Selbst wenn man aufgrund der Natur von eMail-Adressen diese nur schwer richtig validieren kann (kleine Regex können gefährliche Lücken haben oder viel zu restriktiv sein), kann und sollte man immer Newlines und derartige Sonderzeichen aus eMail-Adressen entfernen.

Session Fixation

Bekommt ein Benutzer beim Login eine neue Session-ID?

Session Fixation läuft in etwa wie folgt ab:

Das Beispiel zeigt, dass es notwendig ist, Alice beim Login ein neues Cookie mit einer neuen SID zu geben. Nur dann ist die alte SID von Eve nicht mehr gültig. Das verhindert natürlich nicht, dass Eve Alice das Cookie nicht anderweitig (XSS, ...) stiehlt.

Leitsatz: Wenn sich das Benutzerlevel ändert, generiere eine neue Session-ID.

Security through Obscurity

Gibt es Funktionen auf meiner Seite, die nur dadurch geschützt sind, dass ihr Ort (hoffentlich) einer nicht authorisierten Person nicht bekannt ist?

So verlockend es manchmal auch sein kann, man sollte sich niemals auf derartige „Sicherheit“ verlassen. Da es sich dabei nicht um wirkliche Sicherheit handelt, ist eigentlich schon die Überschrift falsch. Es ähnelt mehr Russischem Roulett mit den Komponenten des Systems.

Vielleicht war der Entwickler so clever, das Godlike-Script „45z3i5u24h2g2h.php“ zu nennen, in der Annahme, dass nur jemand, der diesen Namen kennt, es auch aufrufen kann (und überhaupt erst danach sucht). Dumm nur, dass der Betreiber der Seite aus Versehen seine index.html gelöscht hat und Apache ein Directory Listing erzeugt. Fail.

Okay, das war konstruiert. Warum sollte der Betreiber seine index.html löschen? Okay, die Datei ist da. Mist, in Super-Duper-CMS Version 1.0.9 ist ein Bug: Beim Uploadmanager für Besucher ist eine Directory Traversal-Lücke. Statt /media/ kann ein Angreifer jedes Verzeichnis auflisten lassen. Auch / – und da liegt unser Godlike-Script. Fail.

Wir sehen, die Möglichkeiten sind vielfältig. Der Fehler, der das Kartenhaus des Versteckens in sich zusammenfallen lässt muss nicht einmal bei uns liegen. Selbst wenn wir wissen, dass wir Fehler machen: Wie unrealistisch ist es, weder Fehler in anderen Komponenten noch menschlisches Versagen anzunehmen?

Umgang mit Passwörtern

Speichere ich Passwörter im Klartext?

Der Umgang mit Passwörtern sollte zur Genüge bekannt sein:

Druckansicht