Sichere Dateiuploads mit PHP

4 06 2012

Stellen wir auf unserer Homepage den Besucher über einen Dateiupload die Möglichkeit zur Verfügung, eigene Dateien auf unseren Server zu übertragen, ist eine gewisse Vorsicht anzuraten. Immerhin möchten wir keinen ausführbaren Code auf unseren Server eingeschleust bekommen.

Um die übertragenen Dateien zu prüfen, könnten wir beispielsweise auf die Idee kommen, die $_FILES Variable auszulesen, die nach einem Upload vom Server gefüllt wird. Schauen wir uns diese doch anhand eines kleinen Beispiels einmal an.

<!-- Formular, um eine Datei hochzuladen -->
<form enctype="multipart/form-data" action="index.php" method="post">
Datei hochladen: <input name="datei" type="file" /><br />
<input type="submit" />
</form>

<?php
if (!empty($_FILES)){
	var_dump($_FILES);
}

Ausgabe:

Array
(
    [datei] => Array
        (
            [name] => avatar.jpg
            [type] => image/jpeg
            [tmp_name] => /tmp/phpLgZOUQ
            [error] => 0
            [size] => 8801
        )

)

Im vollständigen Beispiel sähe das dann in etwa so aus:

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('image/gif', 'image/jpeg', 'image/png');	// Erlaubte Dateiformate
	$dateiname = $upload_pfad.basename($_FILES['datei']['name']);	// Dateiname inkl. Pfad

	echo '<p>Erkanntes Dateiformat: '.$_FILES['datei']['type'].'</p>';	// Ausgabe

	if (in_array($_FILES['datei']['type'], $erlaubte_formate)){	// Wenn es sich um eine datei.jpg handelt, gehen wir davon aus, dass es sich um eine Grafik handelt
		if(!move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/'.$_FILES['datei']['name'])) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Ausgabe:

Erkanntes Dateiformat: image/jpeg

<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Nur leider kommen diese Werte vom Browser und lassen sich von daher relativ einfach manipulieren. Wir können uns darauf also nicht verlassen, da wir böswilligen Angreifern somit Tür und Tor öffnen. Die Unzuverlässigkeit erkennen wir deutlich daran, wenn wir unser Jpeg-Bild umbenennen in avatar.exe und den Upload damit testen.

Ausgabe:

Erkanntes Dateiformat: application/x-ms-dos-executable

An der Datei hat sich nichts geändert außer der Erweiterung. Es handelt sich also weiterhin um die gleiche Grafik, nur mit einem fehlerhaften Namenszusatz. Gefährlicher wird es, wenn beispielsweise eine EXE-Datei umbenannt wird, hier könnte einfach eine Bilddatei vorgetäuscht werden.

Somit erkennen wir schon, dass wir uns auch nicht einfach auf die Dateinamenserweiterung verlassen dürfen. Heißt die hochgeladene Datei schadcode.jpg, können wir keineswegs davon ausgehen, dass drin ist, was drauf steht.

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('gif', 'jpg', 'png');	// Erlaubte Dateiformate
	$dateiname = $upload_pfad.basename($_FILES['datei']['name']);	// Dateiname inkl. Pfad

	$dateiinfo = pathinfo($_FILES['datei']['name']);
	$dateiformat = $dateiinfo['extension'];	// Auslesen der Dateierweiterung
	echo '<p>Erkanntes Dateiformat: '.$dateiformat.'</p>';	// Ausgabe

	if (in_array($dateiformat, $erlaubte_formate)){	// Wenn es sich um eine datei.jpg handelt, gehen wir davon aus, dass es sich um eine Grafik handelt
		if(!move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/'.$_FILES['datei']['name'])) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Ausgabe:

Erkanntes Dateiformat: jpg

Auch dies ist keine geeignete Methode, denn die Erweiterung ist schnell verändert. Im Windows lässt sich zwar eine schadcode.jpg nicht starten, weil hier nur bestimmte Dateiformate ausgeführt werden, doch die meisten Webserver laufen unter einem Linux Derivat. Hier ist die Erweiterung vollkommen egal, es kommt nur darauf an, dass die Datei das Executable-Bit gesetzt hat. Im Beispiel sei verdeutlicht, dass auch eine .jpg Datei problemlos ausgeführt werden kann.

$ echo "echo Ich bin ausführbar" >schadcode.jpg
$ chmod u+x schadcode.jpg 
$ ./schadcode.jpg 
Ich bin ausführbar

Von daher sollten wir fremde Dateien niemals auf unserem System ausführen lassen. Idealerweise speichert man diese auch nicht innerhalb der vom Webserver lesbaren Dateistruktur (normalerweise htdocs), so dass diese gar nicht direkt vom Browser aus angesprochen werden können. Über unsere Skripte, die sich ja direkt auf dem Server befinden, sollten je nach Hoster auch andere Verzeichnisse ansprechbar sein. Auf ein Beispiel hierzu verzichte ich, da das etwas zu weit führen würde.

Um das Dateiformat einigermaßen sicher zu bestimmen, müssen wir den MIME Header der Datei lesen. Ab PHP >= 5.3 können wir dazu die finfo_file Funktion mit FILEINFO_MIME_TYPE nutzen. Für ältere PHP Versionen steht das Pendant mime_content_type zur Verfügung.

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('image/gif', 'image/jpeg', 'image/png');	// Erlaubte Dateiformate
	$dateiname = $upload_pfad.basename($_FILES['datei']['name']);	// Dateiname inkl. Pfad

	$finfo = finfo_open(FILEINFO_MIME_TYPE);	// Gib den MIME Typ zurueck
	if ($finfo !== FALSE && !empty($finfo)){
	   	$dateiformat = finfo_file($finfo, $_FILES['datei']['tmp_name']);
		finfo_close($finfo);
	} else die('Fileinfo Datenbank konnte nicht geoeffnet werden');

	echo '<p>Erkanntes Dateiformat: '.$dateiformat.'</p>';	// Ausgabe

	if (in_array($dateiformat, $erlaubte_formate)){	// Dieser Dateityp entspricht unseren Vorgaben
		if(!move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/'.$_FILES['datei']['name'])) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Ausgabe:

Erkanntes Dateiformat: image/jpeg
<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Nun sollte man sich auch nicht auf den Mime-Typ alleine verlassen. So würde nämlich auch eine schadcode.exe Datei akzeptiert werden, wenn nur der Mime-Typ passt. Laden wir doch einfach unsere avatar.exe hoch.

Erkanntes Dateiformat: image/jpeg
<img src="uploads/avatar.exe" title="Hochgeladenes Bild" />

Von daher sollte man die Erweiterung anhand des erkannten Typs stets selbst setzen, um absichtliche wie auch unabsichtliche Nutzerfehler zu vermeiden.

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array(
		'image/gif' => 'gif',
		'image/jpeg' => 'jpg',
		'image/png' => 'png');	// Erlaubte Dateiformate. Im multidimensionalen Array sind nun auch die zugehoerigen Dateierweiterungen angegeben

	$dateiname = $upload_pfad.pathinfo($_FILES['datei']['name'], PATHINFO_FILENAME);	// Mit pathinfo holen wir nur den Dateinamen ohne Erweiterung

	$finfo = finfo_open(FILEINFO_MIME_TYPE);	// Gib den MIME Typ zurueck
	if ($finfo !== FALSE && !empty($finfo) > 0){
	   	$dateiformat = finfo_file($finfo, $_FILES['datei']['tmp_name']);
		finfo_close($finfo);
	} else die('Konnte das Dateiformat nicht erkennen');

	echo '<p>Erkanntes Dateiformat: '.$dateiformat.'<br />';	// Ausgabe

	if (isset($erlaubte_formate[$dateiformat])){	// Existiert ein Eintrag mit dem Key?
		$dateiname .= '.'.$erlaubte_formate[$dateiformat];	// Haenge die Erweiterung an den Dateinamen an

		echo 'Ursprünglicher Dateiname: '.$_FILES['datei']['name'].'<br />';
		echo 'Aktueller Name: '.basename($dateiname).'</p>';

		if(!move_uploaded_file($_FILES['datei']['tmp_name'], $dateiname)) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Die Ausgabe, wenn wir wieder unsere avatar.exe hochladen:

Erkanntes Dateiformat: image/jpeg
Ursprünglicher Dateiname: avatar.exe
Aktueller Name: avatar.jpg

<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Abschließend sei noch auf getID3() hingewiesen. Dieses umfangreiche Skript liest die verschiedensten Multimedia-Formate ein und liefert neben den Header Informationen noch viele weitere nützliche Informationen. In unserem Beispiel sähe der Einsatz dann so aus:

<?php
if (!empty($_FILES)){
	require_once('getid3/getid3.php');	// getID3() einbinden
	$getID3 = new getID3;

	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('image/gif', 'image/jpeg', 'image/png');	// Erlaubte Dateiformate

	$dateiname = $upload_pfad.pathinfo($_FILES['datei']['name'], PATHINFO_FILENAME);	// Mit pathinfo holen wir nur den Dateinamen ohne Erweiterung

	$dateiinfo = $getID3->analyze($_FILES['datei']['tmp_name']);
	var_dump($dateiinfo);

	echo '<p>Erkanntes Dateiformat: '.$dateiinfo['mime_type'].'<br />';	// Ausgabe

	if (in_array($dateiinfo['mime_type'], $erlaubte_formate)){	// Dieser Dateityp entspricht unseren Vorgaben
		$dateiname .= '.'.$dateiinfo['fileformat'];	// Haenge die Erweiterung an den Dateinamen an

		echo 'Ursprünglicher Dateiname: '.$_FILES['datei']['name'].'<br />';
		echo 'Aktueller Name: '.basename($dateiname).'</p>';

		if(!move_uploaded_file($_FILES['datei']['tmp_name'], $dateiname)) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Zur besseren Verdeutlichung erfolgt zunächst die Ausgabe des von getID3() gelieferten Arrays mit den Dateiinformationen. Danach folgt unsere normale Ausgabe. Da auch die Erweiterung geliefert wird, müssen wir diese nicht mehr selbst definieren:

array(11) {
  ["GETID3_VERSION"]=>
  string(14) "1.9.3-20111213"
  ["filesize"]=>
  int(8801)
  ["filename"]=>
  string(9) "phpb1p7Qd"
  ["filepath"]=>
  string(4) "/tmp"
  ["filenamepath"]=>
  string(14) "/tmp/phpb1p7Qd"
  ["avdataoffset"]=>
  int(0)
  ["avdataend"]=>
  int(8801)
  ["fileformat"]=>
  string(3) "jpg"
  ["video"]=>
  array(7) {
    ["dataformat"]=>
    string(3) "jpg"
    ["lossless"]=>
    bool(false)
    ["bits_per_sample"]=>
    int(24)
    ["pixel_aspect_ratio"]=>
    float(1)
    ["resolution_x"]=>
    int(72)
    ["resolution_y"]=>
    int(100)
    ["compression_ratio"]=>
    float(0.4074537037037)
  }
  ["encoding"]=>
  string(5) "UTF-8"
  ["mime_type"]=>
  string(10) "image/jpeg"
}

Erkanntes Dateiformat: image/jpeg
Ursprünglicher Dateiname: avatar.exe
Aktueller Name: avatar.jpg

<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Ist das jetzt wirklich sicher? Einigermaßen, doch muss für die maximale Sicherheit immer der maximale Aufwand betrieben werden. Mit Erkennung des Mime-Headers und idealerweise der Ablage der Dateien außerhalb des Web-Roots sollte man zumindest einigermaßen auf der sicheren Seite sein. Sicherer immerhin, als sich auf die durch $_FILES oder getimagesitze gelieferten Informationen zu verlassen. Letzteres hat sich in der Hinsicht in der Vergangenheit immer wieder durch seine Angreifbarkeit disqualifiziert.

Advertisements

Aktionen

Information

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s




%d Bloggern gefällt das: