Sicherer Dateiupload
Viele Websites erlauben es Nutzern Dateien hochzuladen, beispielsweise Bilddateien. Dies ist allerdings mit einigen Sicherheitsrisiken verbunden. Durch einen unachtsamen Dateiupload können Angreifer beispielsweise PHP-Scripts hochladen und so beliebigen PHP-Code auf eurer Website ausführen. Der nachfolgenden Artikel zeigt die größten Sicherheitsrisiken zum Thema Dateiupload und präsentiert eine Variante für den sicheren Dateiupload.
Inhaltsverzeichnis
Gefahrenquellen
Benutzer den Upload von Dateien zu erlauben birgt mehrere Gefahrenquellen. Zum einen könnten so PHP-Script hochgeladen werden und schadhafter Code ausgeführt werden (siehe Code Injection). Eine weitere Gefahr ist, dass eine .htaccess oder eine php.ini Datei hochgeladen wird und so die Server- bzw. die PHP-Konfiguration geändert wird.
Oftmals will man den Upload von Bildern erlauben, damit Benutzer beispielsweise ein Profilbild hochladen können. Sich hier auf die $_FILES['datei']['type'] zu verlassen ist nicht ausreichend, denn diese kann beliebig vom Benutzer verändert werden. Hier die Dateiendung zu Überprüfen ist empfehlenswert.
Ein weiteres Problem beim Upload von Bildern besteht bzgl. Cross-Site-Scripting (XSS). Lädt ein Besucher eine HTML-Datei mit der Dateiendung .jpg hoch, so werden manche Browser unter Umständen dies als HTML-Datei ausführen und somit ließe sich schadhafter HTML-Code einschleusen.
Im nachfolgenden Artikel werden wir versuchen euch die wichtigsten Elemente eines sicheren Dateiuploads zu erläutern.
Der sichere Dateiupload
Für den sicheren Dateiupload empfehlen sich die folgenden Schritte:
Absicherung des Verzeichnis
Die hochgeladenen Dateien sollten in einen speziellen Ordner hochgeladen werden und dieser Ordner sollte zusätzlich geschützt werden. Wir empfehlen euch die Dateien nach z.B. uploads/files/ hochzuladen. Im Ordner uploads/ solltet ihr eine .htaccess-Datei ablegen, die das Ausführen von Scripts verhindert. Die Lösung mittels dieser zwei Ordern schütz die .htaccess-Datei davor überschrieben zu werden.
Die sicherste Variante ist per .htaccess-Datei sämtlichen Zugriff auf den Ordner zu verbieten.
1 2 |
Order Deny,Allow Deny from All |
Mittels PHP-Script lassen sich dann erlaubte Dateien zum Download / zur Betrachtung darstellen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?php // Aufruf mittels: download.php?file=bild.png // Überprüft, ob bild.php im Ordner 'uploads' existiert. Zeigt dieses Bild an oder bietet es zum Download an if(!isset($_GET['file'])) { die("Bitte eine Datei auswählen"); } $filename = basename($_GET['file']); //Optional: Nur bestimmte Datei-Endungen erlauben $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $allowed_extensions = array('png', 'jpg', 'jpeg', 'gif', 'pdf'); if(!in_array($extension, $allowed_extensions)) { die("Ungültige Dateiendung"); } //Überprüfen ob Datei existiert $filepath = "uploads/".$filename; if(!file_exists($filepath)) { die("Datei existiert nicht"); } //Variante 1: Datei direkt im Browser anzeigen $content_type = mime_content_type($filepath); header('Content-type: '.$content_type); header('Content-Disposition: inline; filename="'.$filename.'"'); readfile($filepath); exit(); //Variante 2: Datei zum Download anbieten header('Content-Type: application/octet-stream'); header('Content-Transfer-Encoding: Binary'); header('Content-disposition: attachment; filename="'.$filename.'"'); readfile($filepath); exit(); |
Sollen Dateien in dem Ordner weiterhin direkt per URL aufrufbar bleiben, so sollten PHP-Scripts und ähnliches deaktiviert werden. Die folgende .htaccess-Datei deaktiviert alle Scripts im Ordern:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# File: uploads/.htaccess # This should prevent ALL files being executed/interpreted/handled. SetHandler none SetHandler default-handler # Disables the execution of cgi-files Options -ExecCGI # Disables the php engine <IfModule mod_php5.c> php_flag engine off </IfModule> |
Überprüfung der Dateiendung
Ihr solltet überprüfen, dass die hochgeladene Datei eine erlaubte Dateiendung besitzt. Es ist sicherer nur gewisse Dateiendungen, z.B. png und jpg, zu erlauben, statt zu versuchen gefährliche Dateiendungen zu begrenzen. Verwenden könnt ihr dazu folgenden Script:
1 2 3 4 5 6 |
$extension = strtolower(pathinfo($_FILES['datei']['name'], PATHINFO_EXTENSION)); $allowed_extensions = array('png', 'jpg', 'jpeg', 'gif'); if(!in_array($extension, $allowed_extensions)) { die("Ungültige Dateiendung"); } |
Überprüfung ob es ein Bild ist
Das Überprüfen der Dateiendung bei Bildern ist nicht ausreichend, Angreifer können durch korrupte Bilddateien unter Umständen weiterhin HTML-Code einschleusen und so ein XSS-Angriff durchführen.
Das Überprüfen der $_FILES['datei']['type']-Angabe ist ebenfalls keine Lösung, da diese beliebig vom Angreifer manipuliert werden kann. Zum Überprüfung des Bildtyps könnt ihr in PHP die Funktion exif_imagetype() nutzen. Diese benötigt allerdings, dass die exinf-Erweiterung installiert ist. Sollte ihr also eine Fehlermeldung erhalten, dass die Funktion exif_imagetype nicht existent ist, so ist bei eurem Server diese Erweiterung leider nicht installiert.
1 2 3 4 5 |
$allowed_types = array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF); $detected_type = exif_imagetype($_FILES['datei']['tmp_name']); if(!in_array($detected_type, $allowed_types)) { die("Nur der Upload von Bilddateien ist gestattet"); } |
Überprüfung der Dateigröße
Um das Zuspammen eures Webserver zu vermeiden solltet ihr die Dateigröße überprüfen:
1 2 3 4 |
$max_size = 500*1024; //500 KB if($_FILES['datei']['size'] > $max_size) { die("Bitte keine Dateien größer 500kb hochladen"); } |
Vergabe eines neuen Namens
Es existieren zwei Möglichkeiten, um zu vermeiden das Dateien um Upload-Order überschrieben werden. Die erste Möglichkeit ist den Namen der Datei zu erweitern, z.B. indem man eine Zahl anhängt. Dies seht ihr im Script weiter unten, dass eine Zahl anhängt falls die Datei bereits existiert.
Die andere Möglichkeit ist es, einen neuen, zufälligen Namen zu vergeben. Den vorherigen Namen kann man beispielsweise in der Datenbank abspeichern und sofern man die Datei per PHP-Script zum Download anbietet, diesen Namen wieder mitsenden. Das Vergeben eines zufälligen Namens hat auch den Vorteil, dass keiner Dateinamen erraten kann und so Bilder/Dateien aus dem Upload-Ordner auslesen kann, die nicht für ihn bestimmt sind. Die Vergabe eines zufälligen Namens kann wie folgt aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
$temporary_name = $_FILES['datei']['tmp_name']; $extension = pathinfo($_FILES['datei']['name'], PATHINFO_EXTENSION); //Überprüfung der Datei-Endung, MIME-Header Check etc. function random_string() { if(function_exists('random_bytes')) { $bytes = random_bytes(16); $str = bin2hex($bytes); } else if(function_exists('openssl_random_pseudo_bytes')) { $bytes = openssl_random_pseudo_bytes(16); $str = bin2hex($bytes); } else if(function_exists('mcrypt_create_iv')) { $bytes = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); $str = bin2hex($bytes); } else { //Bitte euer_geheim_string durch einen zufälligen String mit >12 Zeichen austauschen $str = md5(uniqid('euer_geheimer_string', true)); } return $str; } $name = random_string(); //random new name $new_name = 'uploads/files/'.$name.'.'.$extension; move_uploaded_file($temporary_name, $new_name); echo "Bild hochgeladen nach: $new_name"; |
Beispiel: Sicherer Bildupload
Falls euch dies zu viele Informationen waren hier ein Beispiel für den sicheren Dateiupload von Bildern. Sollte eine Datei mit dem Namen schon vorhanden sein, so wird an den Namen noch eine Zahl angehängt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?php $upload_folder = 'uploads/files/'; //Das Upload-Verzeichnis $filename = pathinfo($_FILES['datei']['name'], PATHINFO_FILENAME); $extension = strtolower(pathinfo($_FILES['datei']['name'], PATHINFO_EXTENSION)); //Überprüfung der Dateiendung $allowed_extensions = array('png', 'jpg', 'jpeg', 'gif'); if(!in_array($extension, $allowed_extensions)) { die("Ungültige Dateiendung. Nur png, jpg, jpeg und gif-Dateien sind erlaubt"); } //Überprüfung der Dateigröße $max_size = 500*1024; //500 KB if($_FILES['datei']['size'] > $max_size) { die("Bitte keine Dateien größer 500kb hochladen"); } //Überprüfung dass das Bild keine Fehler enthält if(function_exists('exif_imagetype')) { //exif_imagetype erfordert die exif-Erweiterung $allowed_types = array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF); $detected_type = exif_imagetype($_FILES['datei']['tmp_name']); if(!in_array($detected_type, $allowed_types)) { die("Nur der Upload von Bilddateien ist gestattet"); } } //Pfad zum Upload $new_path = $upload_folder.$filename.'.'.$extension; //Neuer Dateiname falls die Datei bereits existiert if(file_exists($new_path)) { //Falls Datei existiert, hänge eine Zahl an den Dateinamen $id = 1; do { $new_path = $upload_folder.$filename.'_'.$id.'.'.$extension; $id++; } while(file_exists($new_path)); } //Alles okay, verschiebe Datei an neuen Pfad move_uploaded_file($_FILES['datei']['tmp_name'], $new_path); echo 'Bild erfolgreich hochgeladen: <a href="'.$new_path.'">'.$new_path.'</a>'; ?> |
Und im upload-Verzeichnis zusätzlich noch die folgende .htaccess-Datei ablegen, um das Ausführen von PHP-Scripts zu verhindern:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# File: uploads/.htaccess # This should prevent ALL files being executed/interpreted/handled. SetHandler none SetHandler default-handler # Disables the execution of cgi-files Options -ExecCGI # Disables the php engine <IfModule mod_php5.c> php_flag engine off </IfModule> |