Benutzer:TS/File-Upload

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche
Hauptartikel: PHP/Tutorials/File Upload
Hinweis:
Im Selfhtml-aktuell-Bereich gab es einen Artikel Dateiupload und Überprüfung mit PHP von Ludwig Ruderstaller aus dem Jahre 2001.

In vielen Situationen erscheint es notwendig, mittels HTML-Formular auch Dateien an den Server zu übertragen, also sogenannte Uploads durchzuführen. Hierbei kann es sich um Bilder zum Text, um mitzuliefernde Textdateien, Office-Dokumente oder auch HTML-Seiten oder sogar ausführbare Scripte handeln. Beliebige andere Datenfiles sind denkbar.

Oft wird dabei leider übersehen, dass man sich durch die Bereitstellung eines File-Uploads eine Sicherheitslücke in seinen Server reißen kann.

Dieser Artikel soll daher NICHT zeigen, wie man mal eben schnell einen Fileupload in seine Webseiten einbaut, sondern WARUM MAN NICHT MAL EBEN SCHNELL ein Loch in seinem Server reißen sollte, indem man sich kurzsichtig ein "Uploadscript" aus dubiosen Quellen zusammenkopiert.

Die Fertigstellung dieses Artikels dauert auch etwas länger, da es bisher kaum öffentliche Quellen zu den möglichen Löchern im System gibt und täglich neue Anregungen hinzu kommen. Alles, was hier aber bisher geschrieben steht, lohnt sich gelesen zu werden - auch wenn sich vielleicht im Wandel des Artikels manche Überlegungen schon wieder erledigt haben sollten.

Ich werde mich bemühen, nicht nur die "Uploadseite" zu zeigen, sondern auch praktische Beispiele für die anschließende "Downloadseite", weil man die Problematiken nicht voneinander trennen darf!

Inhaltsverzeichnis

Verfahren

HTTP POST für eine Datei

Die Übermittlung findet in der Regel mit den Protokollen HTTP oder HTTPS statt. Für beide Protokolle stehen zwei Anfragemethoden (POST, PUT) zur Verfügung, von denen meistens nur die POST-Methode implementiert ist.

{ToDo|Im Self-Forum [1] gab es einen Hinweis auf eine weitere (schnelle und schmutzige) Methode, die man in der Praxis nutzen kann, wenn kleine Dateien zwischen einem Host (als Client) und einem anderen Host (als Server) übertragen werden sollen.

Diese Methode soll aber nicht Bestandteil dieses Artikels über "Browser -> Server"-Kommunikation sein, sondern in einem weiteren (ToDo -> Filetransfer zwischen Hostsystemen) behandelt werden. --Matthias Scharwies (Diskussion) 04:06, 18. Okt. 2016 (CEST)}

Client

Bei der Post-Methode verpackt der Browser alle zu übermittelnden Daten in einem Post-Request. Zur Übermittlung von Files benötigt PHP hier auf der Server-Seite den MIME-Encryption-Type multipart/form-data, sodass man den Browser auch auf der Client-Seite anweisen muss, diesen zu benutzen.

In einer validen HTML-Seite notiert man dazu ein form-Element, das seinerseits das input-Element vom Typ file enthält. Es fehlt nun nur noch die Möglichkeit, die Übertragung auszulösen. Dies geschieht am einfachsten durch das Hinzufügen eines button-Elements. Mehr ist auf der Clientseite nicht notwendig.

  • Benötigte Elemente
    • form
      • action
      • method
      • enctype
    • input
      • Typ file mit Attribut name
    • button
      • Typ submit mit Attribut name
Beispiel
Ausschnitt aus dem HTML-Formular:
<form action="upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="dateiupload"> <input type="submit" name="btn[upload]"> </form>
So sieht es im Browserfenster aus:
screenshot
Der Firefox-Browser erzeugt ein Eingabefeld, einen Durchsuchen-Button und einen Button mit der Beschriftung „Daten absenden“. Diesen Text ergänzt er selbsttätig, weil wir kein Value-Attribut für den Button angegeben haben. Mit einem value="mein_Text" kann man dem Submit-Button auch einen eigenen Text zuweisen.
Das macht der Browser beim Absenden daraus:
Ziel-Adresse: http://testserver.lan/upload/Artikel/upload.php
POST /upload/Artikel/upload.php HTTP/1.1 Host: testserver.lan ... (gekürzt) Content-Type: multipart/form-data; boundary=---------------------------3902153292 Content-Length: 464 -----------------------------3902153292 Content-Disposition: form-data; name="dateiupload"; filename="upload.php" Content-Type: application/octet-stream <?php #### upload.php #### if ($_FILES) { echo "<pre>\r\n"; echo htmlspecialchars(print_r($_FILES,1)); echo "</pre>\r\n"; } ?> -----------------------------3902153292 Content-Disposition: form-data; name="btn[upload]" Daten absenden -----------------------------3902153292--
Wie man aus dem Mitschnitt der HTTP-Header sehen kann, findet der HTTP-Upload im Klartext statt. Dateien werden vom Browser als simpler Bytestream eingebunden. Der Server hat hier nur über die Boundary die Möglichkeit, das Ende der übertragenen Datei (hier das für die Kontrolle benutzte PHP-Script) zu erkennen.
Das antwortet der Server (Response-Headers):
HTTP/1.1 200 OK Date: Thu, 08 Apr 2010 08:03:34 GMT ... (gekürzt) Content-Length: 282 Content-Type: text/html; charset=ISO-8859-1 ... (HTML-Dokument)

Server

Der Webserver muss auf einen Upload eingestellt sein. Bei Verwendung von PHP findet dies bei der Installation automatisch statt. Die Methode POST wird von PHP von Haus aus unterstützt.

Außerdem muss in der php.ini der Eintrag file_uploads=on vorhanden sein.

Das superglobale Array $_FILES wird trotzdem immer angelegt, wenn der Browser für den Request ein <input type="file" ... > verarbeitet hat, also im Requestbody "multipart/form-data" und "filename=DATEINAME" vorhanden sind. Dabei ist es unerheblich, ob wirklich ein File übertragen wurde. Dies erkennen wir im Folgenden aus den Fehlercodes.

Verzeichnisstruktur

Um Upload-Daten auf dem Server speichern zu können, muss ein upload_tmp_dir vorhanden sein. Bei Standardeinrichtungen liegt dies meist im shared Temporary Directory. Die Standard-Einrichtung ist für Shared Hosts nicht zu empfehlen, da auch andere VirtHosts Zugriff auf das gemeinsame Verzeichnis /tmp/ nehmen können. Diese können zwar den Dateinamen nicht ändern und die Datei nicht löschen, aber sie können den Inhalt verändern.

Eine empfohlene Verzeichnisstruktur sieht daher wie folgt aus:

Beispiel
/var/www/virthosts/example.org/htdocs
/var/www/virthosts/example.org/data
/var/www/virthosts/example.org/sessions
/var/www/virthosts/example.org/includes
/var/www/virthosts/example.org/tmp
/var/www/virthosts/example.org/logs

Dies muss dann bei der Virtual-Host-Einrichtung auch bei den Einstellungen für open_basedir berücksichtigt werden und wenn ein logrotate eingerichtet ist, muss dies bei Verschiebung der Logs in einen domain-privaten Bereich auch angepasst werden

Auf Debian-Systemen wird dann außerdem zunächst ggf. der Session Garbage Controller nicht mehr greifen, da dieser hier per Cronjob arbeitet und standardmäßig auf /tmp wirkt. Hier kommt es dann auf ein gutes Zusammenspiel zwischen Hoster und Domaininhaber an {ToDo|eigener Artikel: Apache-Tipps/php}

Beispiel
DocumentRoot /var/www/virthosts/example.org/htdocs

php_admin_value open_basedir /var/www/virthosts/example.org/
php_admin_value upload_tmp_dir /var/www/virthosts/example.org/tmp/
php_admin_value session.save_path /var/www/virthosts/example.org/sessions/

# ...

POST auswerten

Die Clients übergeben Files nur mit der POST-Methode.

Das PHP-System (seit 4.x) übergibt alle notwendigen Daten in den „superglobalen Arrays“ $_POST, $_GET, $_FILES usw. an das assozierte Script.

Wir wollen für die Upload-relevanten Daten bitte nur noch diese Arrays, speziell das Array $_FILES benutzen. Alle anderen zur Übergabe bisher verwendeten Parameter und Parameter-Arrays können unsicher sein! Die anderen Datenstrukturen könnten vom Client auch nachhaltig manipuliert sein.

Bei einem Dateiupload benötigen wir dann bei PHP nur sehr wenige Zeilen, um an die notwendigen Informationen für die Weiterverarbeitung zu kommen. Dies verleitet leider immer noch allzu leicht dazu, sich dieser Weiterverarbeitung nicht wenigstens ausreichend zu widmen und so entstehen viele ungenügende Scripte, die große Sicherheitslücken in die Webserver reißen. Anschließend stehen die betroffenen Server dann für Missbrauch zur Verfügung.

Um uns erst einmal einen Überblick über die ankommenden Daten zu machen, benötigen wir nur ein kleines Testscript:

Beispiel
<?php  #### upload.php ####

if (!empty($_FILES))
{
    echo "<pre>\r\n";
    echo htmlspecialchars(print_r($_FILES,1));
    echo "</pre>\r\n";
}

?>

Hinweis: Warum "!empty($_FILES)"? Wenn der Client kein "multipart/form-data" und im entsprechenden Abschnitt kein filename="FILENAME" überträgt, wird das Array $_FILES nicht angelegt.


Benutzt man dieses Testscript durch den vorhin bereits gezeigten Request des Browsers, antwortet der Server mit der nachfolgenden Response (zusätzlich zu den oben schon beschriebenen Headers):


Array
(
    [dateiupload] => Array
        (
            [name] => <selected file>
            [type] => application/octet-stream
            [tmp_name] => C:\Programme\xampp\tmp\php4D8.tmp
            [error] => 0
            [size] => 150
        )

)


Abgebildet wird hier durch das kleine Script das superglobale Array $_FILES. Dieses enthält ein Element [dateiupload]. Der Name des Elementes rührt vom Name-Attribut des <input>-Elementes vom Typ File aus dem HTML-Dokument, das wir für den Client gebaut hatten.

Das Element [dateiupload] enthält nun wieder Unterelemente und ihre Werte, die von PHP angelegt wurden:

  • [name] enthält den Namen der Datei, den der Client mitgesendet hat, kann aber auch einen ganzen Pfad enthalten, ist daher unsicher
  • [type] enthält den MIME-Type der Datei, den der Client behauptet, ist daher unsicher
  • [tmp_name] enthält den Pfad zur temporären Datei auf dem Webserver, wird vom Webserver vergeben, ist nicht von außen überschreibbar und daher sicher
  • [error] enthält Errorcodes laut Spezifikation, wird vom Webserver ermittelt und ist daher sicher
  • [size] enthält die Größe der Temporärdatei in Bytes, wird vom Webserver ermittelt und ist daher sicher

Als „sicher“ werden hier nur die zur Verfügung stehenden Werte betrachtet, nicht jedoch der Inhalt des Upload-Files.

Zur Weiterverarbeitung der hochgeladenen Daten bauen wir uns eine Funktion (oder bei OOP eine Methode), die alle zu berücksichtigen Prüfungen vornehmen wird, die hochgeladene Datei an ihren Speicherort verschiebt und uns am Ende mit einem true oder false über den Erfolg informiert.

Error-Codes des Uploads

siehe http://www.php.net/manual/en/features.file-upload.errors.php


UPLOAD_ERR_OK         Value: 0; There is no error, the file uploaded with success.

UPLOAD_ERR_INI_SIZE   Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini.

UPLOAD_ERR_FORM_SIZE  Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that 
                                was specified in the HTML form.

UPLOAD_ERR_PARTIAL    Value: 3; The uploaded file was only partially uploaded.

UPLOAD_ERR_NO_FILE    Value: 4; No file was uploaded.

UPLOAD_ERR_NO_TMP_DIR Value: 6; Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.

UPLOAD_ERR_CANT_WRITE Value: 7; Failed to write file to disk. Introduced in PHP 5.1.0.

UPLOAD_ERR_EXTENSION  Value: 8; A PHP extension stopped the file upload. PHP does not provide a way 
                                to ascertain which extension caused the file upload to stop; examining 
                                the list of loaded extensions with phpinfo() may help. 
                                Introduced in PHP 5.2.0.


Der Parameter MAX_FILE_SIZE

PHP bietet die (unsichere) Möglichkeit, jedem hochzuladenden File eine eigene Größenbegrenzung zuzuordnen.

<input type="hidden" name="MAX_FILE_SIZE" value="4096">

Diese Angabe muss dann vor dem jeweiligen <input type="file" ...>-Element stehen und gilt solange für alle darauffolgenden Upload-Elemente, bis sie durch eine neue Angabe überschrieben wird.

PHP bricht bei Erreichen der angegeben Grenze in Bytes die Übernahme des Files ab und verwirft es. Als Error-Code erscheint dann UPLOAD_ERR_FORM_SIZE ( === 2 ) im dazugehörigen Error-Element des $_FILES-Arrays.

Da die Angabe aber vom Client kommt, ist sie unsicher. Im Zweifel gilt daher für jedes File die Direktive upload_max_filesize aus der php.ini, der VirtHost-Konfiguration oder einer .htaccess-Direktive. Siehe auch http://de2.php.net/manual/en/ini.core.php#ini.upload-max-filesize

Für alle Files zusammen inklusive sonstigen Post-Parametern gilt die Grenze post_max_size Siehe auch http://de2.php.net/manual/en/ini.core.php#ini.post-max-size

HTTP Post mit mehreren Dateien

Für den Post mit mehreren Dateien stellt uns PHP mehrere Möglichkeiten zur Verfügung, von denen wir zwei zeigen wollen:

Beispiel

Array-Variante:

    <form id="upload" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post" enctype="multipart/form-data"
        onSubmit="document.getElementById('btn_send').setAttribute('disabled','disabled');">

        <input id="file_01" type="file" name="file[1]" size="50">
	<input id="file_02" type="file" name="file[2]" size="50">
	<input id="file_03" type="file" name="file[3]" size="50">
	<input id="file_04" type="file" name="file[4]" size="50">
	
	<input id="btn_send" type="submit" name="btn[send]" value="senden">	
	
    </form>

Man kann die Name-Attribute auch als file[] notieren. PHP nummeriert die Array-Elemente auf dem Server dann selbsttätig durch, beginnend bei 0.



Beispiel

Diskrete Namen:

    <form id="upload" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post" enctype="multipart/form-data"
        onSubmit="document.getElementById('btn_send').setAttribute('disabled','disabled');">

        <input id="file_01" type="file" name="file_01" size="50">
	<input id="file_02" type="file" name="file_02" size="50">
	<input id="file_03" type="file" name="file_03" size="50">
	<input id="file_04" type="file" name="file_04" size="50">
	
	<input id="btn_send" type="submit" name="btn[send]" value="senden">	
	
    </form>


Der Unterschied zeigt sich dann mittels unseres Testscriptes auf dem Server:

Beispiel

Array-Variante:

Array
(
    [file] => Array
        (
            [name] => Array
                (
                    [1] => 35mm-filmstreifen.jpg
                    [2] => Blockhaus_innen_Saal.jpg
                    [3] => lupe.gif
                    [4] => Satelliten-Foto (Google-Maps).bmp
                )

            [type] => Array
                (
                    [1] => image/jpeg
                    [2] => image/jpeg
                    [3] => image/gif
                    [4] => 
                )

            [tmp_name] => Array
                (
                    [1] => M:\User\Tom\www\testserver.lan\tmp\php12F.tmp
                    [2] => M:\User\Tom\www\testserver.lan\tmp\php130.tmp
                    [3] => M:\User\Tom\www\testserver.lan\tmp\php131.tmp
                    [4] => 
                )

            [error] => Array
                (
                    [1] => 0
                    [2] => 0
                    [3] => 0
                    [4] => 1
                )

            [size] => Array
                (
                    [1] => 12645
                    [2] => 112716
                    [3] => 1373
                    [4] => 0
                )

        )

)

Auf den Fehler beim File Nr. 4 kommen wir gleich zurück.


Beispiel

Diskrete Namen:

Array
(
    [file_01] => Array
        (
            [name] => Blockhaus_Plan_EG.jpg
            [type] => image/jpeg
            [tmp_name] => M:\User\Tom\www\testserver.lan\tmp\php13C.tmp
            [error] => 0
            [size] => 38118
        )

    [file_02] => Array
        (
            [name] => 35mm-filmstreifen_br.jpg
            [type] => image/jpeg
            [tmp_name] => M:\User\Tom\www\testserver.lan\tmp\php13D.tmp
            [error] => 0
            [size] => 9901
        )

    [file_03] => Array
        (
            [name] => lupe.gif
            [type] => image/gif
            [tmp_name] => M:\User\Tom\www\testserver.lan\tmp\php13E.tmp
            [error] => 0
            [size] => 1373
        )

    [file_04] => Array
        (
            [name] => Rad_01.jpg
            [type] => image/jpeg
            [tmp_name] => M:\User\Tom\www\testserver.lan\tmp\php13F.tmp
            [error] => 0
            [size] => 3927
        )

)


PHP baut das $_FILES-Array unterschiedlich auf, je nachdem, ob der Client Array-Bezeichner oder Skalare als Bezeichner benutzt. Wenn man das Ganze mischt, wird es noch verrückter!


Als erstes sollte der Errorcode im Element $_FILES['dateiupload']['error'] überprüft werden

Beispiel
    ### Skalare Namensangabe: ###
    if (isset($_FILES[$fileupload]['error']))
    { 
        if ($_FILES[$fileupload]['error'] === UPLOAD_ERR_OK)
        {
            file_process($_FILES[$fileupload]);   ### Verarbeitungsfunktion erstellen wir im Folgenden
        }
        ### Namensangabe als Array ### 
        elseif (is_array($_FILES[$fileupload]['error']))
        {
            foreach($_FILES[$fileupload]['error'] as $key => $error)
            {
                if ($error === UPLOAD_ERR_OK)
                {
                    $_filerec = array();
                    $_filerec['name'] = $_FILES[$fileupload]['name'][$key];
                    $_filerec['type'] = $_FILES[$fileupload]['type'][$key];
                    $_filerec['tmp_name'] = $_FILES[$fileupload]['tmp_name'][$key];
                    $_filerec['error'] = $_FILES[$fileupload]['error'][$key];
                    $_filerec['size'] = $_FILES[$fileupload]['size'][$key];

                    file_process($_filerec);
                }
            } 
        }
        else
        {
            ### Fehlerbearbeitung, oder
            return false;
        }
    }

Wenn die Abfrage des [error]-Elementes genau (Identitätsvergleich) dem Wert UPLOAD_ERR_OK entspricht, dann sagt uns PHP damit, dass unter dem Elementnamen [dateiupload] des $_FILES-Arrays genau eine Datei mit dem letzten (dem Script zugeordneten) Post-Request fehlerfrei auf den Server hochgeladen wurde (der Upload mehrerer Dateien wird später betrachtet). Der Server hat diese Datei im Temporärverzeichnis abgelegt und dort ist sie nun für das Script unter dem im Element [tmp_name] abgelegten vollständigen Pfad erreichbar.

Solange sich die Datei im Temporärverzeichnis befindet, könnte sie dort von anderen Anwendungen, z.B. PHP-Instanzen anderer User, manipuliert werden. Auf Linux-Systemen kann sie zwar in der Regel nicht umbenannt oder gelöscht werden (gesetztes t-Flag), aber ihr Inhalt ist nicht sicher. Um dies zu vermeiden, sollte in einer Shared-Hosting-Umgebung die Einstellung für upload_tmp_dir unbedingt für jede Anwendung / jedes Projekt auf ein eigenes Verzeichnis zeigen, das für andere PHP-Prozesse nicht erreichbar ist. Das Temporärverzeichnis sollte unbedingt auch außerhalb der Document Root liegen! Dies muss dann auch durch eine passende Einstellung von open_base_dir berücksichtigt werden, damit das Verzeichnis für das Script erreichbar ist.

Wir könnten die Datei nun einfach aus dem Temporärverzeichnis an eine andere Stelle innerhalb unseres für PHP zugänglichen Dateibaumes kopieren.

Aber genau das wollen wir nicht ungeprüft tun!


{ToDo| [Funktionsrumpf Multi-Upload auswerten] }

Übernahme aus anderen Uploadverfahren

In Netzwerkbetriebssystemen, speziell im Internetverbund, gibt es für Dateiuploads und -transfers selbstverständlich nicht nur HTTP/s, sondern diverse andere Verfahren:

  • FTP
  • sFTP
  • scp
  • ssh (Copy & Paste über die Konsole)
  • sshfs
  • rsync
  • usw.

Hier muss man selbstverständlich auch für Sicherheit sorgen. Die Sicherheitsbetrachtungen der einzelnen Verfahren gehören nicht zu diesem "never ending article", aber die Ankoppelung mit PHP an die betroffenen Verzeichnisse soll betrachtet werden.

Sicherheit

Wer es zulässt, dass auf seinen Server Daten oder Dateien hochgeladen werden, riskiert immer die Sicherheit seines Systems. Es ist daher unbedingt notwendig, sich genaue Gedanken über den Verbleib der Daten auf dem Server und den Umgang damit zu machen.

Eine grundsätzliche Frage muss daher immer geklärt werden:

Können hochgeladene Daten oder auch nur Teile davon zu irgendeinem Zeitpunkt die Programmkontrolle erlangen?


Sauberen Upload erkennen

Unter "sauberer Upload" verstehen wir hier, dass eine Datei technisch ordnungsgemäß auf unser Zielsystem übertragen werden konnte. Dies beinhaltet die Konsistenz der Daten und die Zuordnungsfähigkeit zu einem Thread (Request). Dies beinhaltet erst einmal nicht die Gefahrlosigkeit der Datei. Diese müssen wir im weiteren diskret untersuchen.

MIME-Type erkennen

Der MIME-Type einer Datei kennzeichnet in etwa ihre Intention, also für welchen Zweck sie vorgesehen ist. So sollten neben der Weiterverarbeitung z.B. Bilder zur Anzeige in einem Grafikprogramm (oder dem integrierten Grafiktool des Browsers), MS-Word-Dateien zur Anzeige mit MS-Word, PDF-Dateien zur Ansicht in einem PDF-Reader, usw. bereitgestellt werden. Bilder sollten aber nicht zur Ausführung als PHP-Datei auf dem Server führen! Genau dies ist aber bei schlampiger Behandlung möglich.

Für die Erkennung des "Multipurpose Internet Mail Extensions"-Formats (MIME-Type) einer Datei ist eine serverseitige Erkennung notwendig. Die vom Client gemeldeten MIME-Types in $_FILES['inputname']['type'] sind für die Absicherung nicht geeignet, da diese Angabe beliebig gefälscht sein kann.

Eine MIME-Type-Erkennung auf dem Server ist aber auch kein Allheilmittel, da z.B. eine eindeutig als JPEG-Datei erkannte Datei durchaus in ihrem EXIF-Header immer noch ein vollwertiges (PHP-)Script enthalten kann.

PHP hat für die MIME-Type-Bestimmung die Funktion mime_content_type() (Handbuch) angeboten, die aber seit längerem schon auf der Unerwünscht-Liste steht. Leider enthält die alternativ dafür aufgenommene PECL extension Fileinfo diverse Unzulänglichkeiten und zusätzliche Unsicherheiten, wie man durch einfache Suchmaschinen-Recherche feststellen wird. Dies mag ein Grund dafür sein, dass die Funktion mime_content_type() immer noch existiert.

Seitdem die Fileinfo-Funktionen Modul des normalen PHP-Sprachumfangs sind (ab PHP 5.3.0), sind auch diese ohne größere Bedenken nutzbar. Sie sind allerdings etwas aufwändiger, als die Funktion mime_content_type()

Wenn man leider über keine der Funktionen verfügt, kann man sich die Funktion mime_content_type() ggf. auch selber bauen:

Beispiel
<?php   #### mime_content_type.php ####

if(!function_exists('mime_content_type'))
{
    function mime_content_type($filename, &$errortxt='')
    {
        #########################################################
        ## Please do not use any direct user input for $filename
        #########################################################
        
        ## for use on windows systems please install first:
        ## http://gnuwin32.sourceforge.net/packages/file.htm
        $path = '';
        if (isset($_SERVER['WINDIR']))
        {
            $path = "C:/Programme/GnuWin32/bin/";
        }    

        $filepath = realpath($filename);
        $_mime = array();

        ## escape spaces in $filename due to their separating effect 
        $filepath = str_replace(" ","\\ ",$filepath);

        exec ($path . "file -bi $filepath", $_mime, $error);

        if (($error) or (count($_mime) != 1)) return false;

        if (strpos($_mime[0], "can't stat") !== false)
        {
            $errortxt = "unknown type";
            $mime = false;
        }
        elseif (strpos($_mime[0], "can't read") !== false)
        {
            $errortxt = "cannot read file";
            $mime = false;
        }
        elseif (strpos($_mime[0], "can't ") !== false)
        {
            $errortxt = "unspecified error";
            $mime = false;
        }
        else
        {
            $mime = trim($_mime[0]);
        }

        return $mime;
    }
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# php main (test section)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

$type = mime_content_type('image.jpg', $errortxt);

## Ausgabe im Browser vorbereiten
header('content-type: text/plain');

var_dump($type) . "\r\n";
var_dump($errortxt) . "\r\n";

?>


Die Funktion leistet nun ungefähr das Gleiche, wie das Original aus PHP. Das Verhalten unter Windows (Fehlermeldungen) ist jedoch noch nicht abgesichert...

Unter der Verwendung von finfo() kann man sich die notwendigen Schritte auch zu einer Funktion zusammenfassen:

Beispiel
### Beispielfunktion unter Verwendung von finfo() ###

function get_mime_type($path)
{
	if (version_compare(PHP_VERSION, '5.3.0') < 0) return false;	
	if (!function_exists('finfo_open')) return false;

	if (!$finfo = finfo_open(FILEINFO_MIME_TYPE)) return false;
	$mime_type = finfo_file($finfo, $path);
	finfo_close($finfo);	

	return $mime_type;
}


Beim Abspeichern der Datei sollte man ihr eine Extension geben, die auf den MIME-Type rückschließen lässt. Diese Extension, bzw. der MIME-Type sollten in einer Positivliste erlaubter Typen enthalten sein. Die Webserver starten üblicherweise diverse Handler über die Dateiendungen.

Eindeutig falsche Extensions sollten auf jeden Fall ersetzt werden durch solche, die eher zutreffend sind. Sind diese dann nicht in der Positivliste enthalten, gehört das File in die Quarantäne! (Dazu auch mehr unter Bilder erkennen).

Eine ausschließlich schreibintensive Funktion, mit der man aus dem eben ermittelten MIME-Type auch _eine_ (von mehreren möglichen) passende Dateiendung ermitteln kann, folgt hier:

Beispiel
### Mögliche Extension aus dem MIME-Type ermitteln

function mime_to_ext($mime_type)
{
	$_mimetypes['application/acad'] 		= 'dwg';	#		AutoCAD
	$_mimetypes['application/applefile'] 		= ''; 		# 		AppleFile-Dateien
	$_mimetypes['application/astound'] 		= 'asd';	# *.asn 	Astound-Dateien
	$_mimetypes['application/dsptype']  		= 'tsp'; 	# 		TSP-Dateien
	$_mimetypes['application/dxf']		 	= 'dxf'; 	#		AutoCAD-Dateien (nach CERN)
	$_mimetypes['application/futuresplash'] 	= 'spl'; 	#		Flash Futuresplash-Dateien
	$_mimetypes['application/gzip'] 		= 'gz';	 	#		GNU Zip-Dateien
	$_mimetypes['application/listenup'] 		= 'ptlk'; 	#		Listenup-Dateien
	$_mimetypes['application/mac-binhex40']		= 'hqx'; 	#		Macintosh Binärdateien
	$_mimetypes['application/mbedlet'] 		= 'mbd'; 	#		Mbedlet-Dateien
	$_mimetypes['application/mif'] 			= 'mif'; 	#		FrameMaker Interchange Format Dateien
	$_mimetypes['application/msexcel'] 		= 'xls'; 	# *.xla 	Microsoft Excel Dateien
	$_mimetypes['application/mshelp'] 		= 'hlp'; 	# *.chm 	Microsoft Windows Hilfe Dateien
	$_mimetypes['application/mspowerpoint'] 	= 'ppt'; 	# *.ppz *.pps *.pot 	Microsoft Powerpoint Dateien
	$_mimetypes['application/msword'] 		= 'doc'; 	# *.dot 	Microsoft Word Dateien
	$_mimetypes['application/octet-stream'] 	= 'bin'; 	# *.exe *.com *.dll *.class 	Nicht näher spezifizierte Binärdaten, z.B. ausführbare Dateien
	$_mimetypes['application/oda'] 			= 'oda'; 	# 		Oda-Dateien
	$_mimetypes['application/pdf'] 			= 'pdf'; 	#		Adobe PDF-Dateien
	$_mimetypes['application/postscript'] 		= 'ai'; 	# *.eps *.ps 	Adobe PostScript-Dateien
	$_mimetypes['application/rtc'] 			= 'rtc'; 	#		RTC-Dateien
	$_mimetypes['application/rtf'] 			= 'rtf'; 	# 		Microsoft RTF-Dateien
	$_mimetypes['application/studiom'] 		= 'smp'; 	#		Studiom-Dateien
	$_mimetypes['application/toolbook'] 		= 'tbk'; 	#		Toolbook-Dateien
	$_mimetypes['application/vocaltec-media-desc'] 	= 'vmd'; 	#		Vocaltec Mediadesc-Dateien
	$_mimetypes['application/vocaltec-media-file'] 	= 'vmf'; 	#		Vocaltec Media-Dateien
	$_mimetypes['application/xhtml+xml'] 		= 'htm'; 	# *.html *.shtml *.xhtml 	XHTML-Dateien
	$_mimetypes['application/xml'] 			= 'xml'; 	#		XML-Dateien
	$_mimetypes['application/x-bcpio'] 		= 'bcpio'; 	# 		BCPIO-Dateien
	$_mimetypes['application/x-compress'] 		= 'z'; 		#		zlib-komprimierte Dateien
	$_mimetypes['application/x-cpio'] 		= 'cpio'; 	# 		CPIO-Dateien
	$_mimetypes['application/x-csh'] 		= 'csh'; 	#		C-Shellscript-Dateien
	$_mimetypes['application/x-director'] 		= 'dcr'; 	# *.dir *.dxr 	Macromedia Director-Dateien
	$_mimetypes['application/x-dvi'] 		= 'dvi'; 	#		DVI-Dateien
	$_mimetypes['application/x-envoy'] 		= 'evy'; 	#		Envoy-Dateien
	$_mimetypes['application/x-gtar'] 		= 'gtar'; 	# 		GNU tar-Archivdateien
	$_mimetypes['application/x-hdf'] 		= 'hdf'; 	#		HDF-Dateien
	$_mimetypes['application/x-httpd-php'] 		= 'php';	# *.phtml 	PHP-Dateien
	$_mimetypes['application/x-javascript'] 	= 'js'; 	#		serverseitige JavaScript-Dateien
	$_mimetypes['application/x-latex'] 		= 'latex'; 	#		LaTeX-Quelldateien
	$_mimetypes['application/x-macbinary'] 		= 'bin'; 	#		Macintosh Binärdateien
	$_mimetypes['application/x-mif'] 		= 'mif'; 	# 		FrameMaker Interchange Format Dateien
	$_mimetypes['application/x-netcdf'] 		= 'nc'; 	# *.cdf 	Unidata CDF-Dateien
	$_mimetypes['application/x-nschat'] 		= 'nsc'; 	# 		NS Chat-Dateien
	$_mimetypes['application/x-sh'] 	    	= 'sh'; 	#		Bourne Shellscript-Dateien
	$_mimetypes['application/x-shar'] 	    	= 'shar'; 	#		Shell-Archivdateien
	$_mimetypes['application/x-shockwave-flash'] 	= 'swf';	# *.cab Flash Shockwave-Dateien
	$_mimetypes['application/x-sprite'] 		= 'spr'; 	# *.sprite 	Sprite-Dateien
	$_mimetypes['application/x-stuffit'] 		= 'sit'; 	#		Stuffit-Dateien
	$_mimetypes['application/x-supercard'] 		= 'sca'; 	#		Supercard-Dateien
	$_mimetypes['application/x-sv4cpio'] 		= 'sv4cpio'; 	# 		CPIO-Dateien
	$_mimetypes['application/x-sv4crc'] 		= 'sv4crc'; 	# 		CPIO-Dateien mit CRC
	$_mimetypes['application/x-tar'] 	    	= 'tar'; 	#		tar-Archivdateien
	$_mimetypes['application/x-tcl'] 	   	= 'tcl'; 	#		TCL Scriptdateien
	$_mimetypes['application/x-tex'] 	    	= 'tex';	# 		TeX-Dateien
	$_mimetypes['application/x-texinfo'] 		= 'texinfo'; 	# *.texi 	Texinfo-Dateien
	$_mimetypes['application/x-troff'] 	    	= 't'; 		# *.tr *.roff 	TROFF-Dateien (Unix)
	$_mimetypes['application/x-troff-man'] 		= 'man'; 	# *.troff 	TROFF-Dateien mit MAN-Makros (Unix)
	$_mimetypes['application/x-troff-me'] 		= 'me'; 	# *.troff 	TROFF-Dateien mit ME-Makros (Unix)
	$_mimetypes['application/x-troff-ms'] 		= 'me'; 	# *.troff 	TROFF-Dateien mit MS-Makros (Unix)
	$_mimetypes['application/x-ustar'] 		= 'ustar'; 	# 		tar-Archivdateien (Posix)
	$_mimetypes['application/x-wais-source'] 	= 'src'; 	# 		WAIS Quelldateien
	$_mimetypes['application/x-www-form-urlencoded'] = '';		#  		HTML-Formulardaten an CGI
	$_mimetypes['application/zip'] 			= 'zip'; 	# 		ZIP-Archivdateien
	$_mimetypes['audio/basic'] 			= 'au'; 	# *.snd 	Sound-Dateien
	$_mimetypes['audio/echospeech'] 		= 'es'; 	#		Echospeed-Dateien
	$_mimetypes['audio/tsplayer'] 			= 'tsi'; 	# 		TS-Player-Dateien
	$_mimetypes['audio/voxware'] 			= 'vox'; 	# 		Vox-Dateien
	$_mimetypes['audio/x-aiff'] 			= 'aif'; 	# *.aiff *.aifc AIFF-Sound-Dateien
	$_mimetypes['audio/x-dspeeh'] 			= 'dus'; 	# *.cht 	Sprachdateien
	$_mimetypes['audio/x-midi'] 			= 'mid'; 	# *.midi 	MIDI-Dateien
	$_mimetypes['audio/x-mpeg'] 			= 'mp2'; 	#		MPEG-Dateien
	$_mimetypes['audio/x-pn-realaudio'] 		= 'ram'; 	# *.ra 		RealAudio-Dateien
	$_mimetypes['audio/x-pn-realaudio-plugin'] 	= 'rpm'; 	# 		RealAudio-Plugin-Dateien
	$_mimetypes['audio/x-qt-stream'] 		= 'stream'; 	# 		Quicktime-Streaming-Dateien
	$_mimetypes['audio/x-wav'] 			= 'wav'; 	#		WAV-Dateien
	$_mimetypes['drawing/x-dwf'] 			= 'dwf'; 	# 		Drawing-Dateien
	$_mimetypes['image/cis-cod'] 			= 'cod'; 	# 		CIS-Cod-Dateien
	$_mimetypes['image/cmu-raster'] 		= 'ras'; 	#		CMU-Raster-Dateien
	$_mimetypes['image/fif'] 			= 'fif'; 	# 		FIF-Dateien
	$_mimetypes['image/gif'] 			= 'gif'; 	#		GIF-Dateien
	$_mimetypes['image/ief'] 			= 'ief'; 	#		IEF-Dateien
	$_mimetypes['image/jpeg'] 			= 'jpg';     	# *.jpeg *.jpe 	JPEG-Dateien
	$_mimetypes['image/png'] 			= 'png'; 	# 		PNG-Dateien
	$_mimetypes['image/tiff'] 			= 'tif';        # *.tiff  	TIFF-Dateien
	$_mimetypes['image/vasa'] 			= 'mcf'; 	# 		Vasa-Dateien
	$_mimetypes['image/vnd.wap.wbmp'] 		= 'wbmp'; 	# 		Bitmap-Dateien (WAP)
	$_mimetypes['image/x-freehand'] 		= 'fh4'; 	# *.fh5 *.fhc 	Freehand-Dateien
	$_mimetypes['image/x-icon'] 			= 'ico'; 	# 		Icon-Dateien (z.B. Favoriten-Icons)
	$_mimetypes['image/x-portable-anymap'] 		= 'pnm'; 	# 		PBM Anymap Dateien
	$_mimetypes['image/x-portable-bitmap'] 		= 'pbm'; 	# 		PBM Bitmap Dateien
	$_mimetypes['image/x-portable-graymap'] 	= 'pgm'; 	# 		PBM Graymap Dateien
	$_mimetypes['image/x-portable-pixmap'] 		= 'ppm'; 	# 		PBM Pixmap Dateien
	$_mimetypes['image/x-rgb'] 			= 'rgb'; 	# 		RGB-Dateien
	$_mimetypes['image/x-windowdump'] 		= 'xwd'; 	# 		X-Windows Dump
	$_mimetypes['image/x-xbitmap'] 			= 'xbm'; 	# 		XBM-Dateien
	$_mimetypes['image/x-xpixmap'] 			= 'xpm'; 	# 		XPM-Dateien
	$_mimetypes['message/external-body'] 		= ''; 		# 		Nachricht mit externem Inhalt
	$_mimetypes['message/http'] 	  		= '';		# 		HTTP-Headernachricht
	$_mimetypes['message/news'] 	  		= '';		# 		Newsgroup-Nachricht
	$_mimetypes['message/partial'] 	  		= '';		# 		Nachricht mit Teilinhalt
	$_mimetypes['message/rfc822'] 	  		= '';		# 		Nachricht nach RFC 2822
	$_mimetypes['model/vrml'] 			= 'wrl'; 	# 		Visualisierung virtueller Welten (VRML)
	$_mimetypes['multipart/alternative'] 		= '';  		# 		mehrteilige Daten gemischt
	$_mimetypes['multipart/byteranges'] 		= '';  		# 		mehrteilige Daten mit Byte-Angaben
	$_mimetypes['multipart/digest'] 	  	= '';		# 		mehrteilige Daten / Auswahl
	$_mimetypes['multipart/encrypted'] 	  	= '';		# 		mehrteilige Daten verschlüsselt
	$_mimetypes['multipart/form-data'] 	  	= '';		# 		mehrteilige Daten aus HTML-Formular (z.B. File-Upload)
	$_mimetypes['multipart/mixed'] 	  		= '';		# 		mehrteilige Daten gemischt
	$_mimetypes['multipart/parallel'] 	  	= '';		# 		mehrteilige Daten parallel
	$_mimetypes['multipart/related'] 	  	= '';		# 		mehrteilige Daten / verbunden
	$_mimetypes['multipart/report'] 	  	= '';		# 		mehrteilige Daten / Bericht
	$_mimetypes['multipart/signed'] 	  	= '';		# 		mehrteilige Daten / bezeichnet
	$_mimetypes['multipart/voice-message'] 		= '';  		# 		mehrteilige Daten / Sprachnachricht
	$_mimetypes['text/comma-separated-values'] 	= 'csv'; 	# 		kommaseparierte Datendateien
	$_mimetypes['text/css'] 			= 'css'; 	# 		CSS Stylesheet-Dateien
	$_mimetypes['text/html'] 			= 'htm'; 	# *.html *.shtml 	HTML-Dateien
	$_mimetypes['text/javascript'] 			= 'js';		# 		JavaScript-Dateien
	$_mimetypes['text/plain'] 			= 'txt'; 	# 		reine Textdateien
	$_mimetypes['text/richtext'] 			= 'rtx'; 	# 		Richtext-Dateien
	$_mimetypes['text/rtf'] 			= 'rtf';	# 		Microsoft RTF-Dateien
	$_mimetypes['text/x-php'] 			= 'php';	# 		PHP-Script-Dateien
	$_mimetypes['text/tab-separated-values'] 	= 'tsv'; 	# 		tabulator-separierte Datendateien
	$_mimetypes['text/vnd.wap.wml'] 		= 'wml'; 	# 		WML-Dateien (WAP)
	$_mimetypes['application/vnd.wap.wmlc'] 	= 'wmlc'; 	# 		WMLC-Dateien (WAP)
	$_mimetypes['text/vnd.wap.wmlscript'] 		= 'wmls'; 	# 		WML-Scriptdateien (WAP)
	$_mimetypes['application/vnd.wap.wmlscriptc'] 	= 'wmlsc'; 	# 		WML-Script-C-dateien (WAP)
	$_mimetypes['text/xml'] 			= 'xml'; 	# 		XML-Dateien
	$_mimetypes['text/xml-external-parsed-entity']  = ''; 		# 		extern geparste XML-Dateien
	$_mimetypes['text/x-setext'] 			= 'etx'; 	# 		SeText-Dateien
	$_mimetypes['text/x-sgml'] 			= 'sgm'; 	# *.sgml 	SGML-Dateien
	$_mimetypes['text/x-speech'] 			= 'talk'; 	# *.spc 	Speech-Dateien
	$_mimetypes['video/mpeg'] 			= 'mpeg'; 	# *.mpg *.mpe 	MPEG-Dateien
	$_mimetypes['video/quicktime'] 			= 'qt'; 	# *.mov 	Quicktime-Dateien
	$_mimetypes['video/vnd.vivo'] 			= 'viv'; 	# *.vivo 	Vivo-Dateien
	$_mimetypes['video/x-msvideo'] 			= 'avi'; 	# Microsoft AVI-Dateien
	$_mimetypes['video/x-sgi-movie'] 		= 'movie'; 	# Movie-Dateien
	$_mimetypes['workbook/formulaone'] 		= 'vts'; 	# *.vtts 	FormulaOne-Dateien
	$_mimetypes['x-world/x-3dmf'] 			= '3dmf'; 	# *.3dm *.qd3d *.qd3 	3DMF-Dateien
	$_mimetypes['x-world/x-vrml'] 			= 'wrl';	# ?

        ### hier proprietäre MIME-Types einfügen
	
	if (isset($_mimetypes[$mime_type]))
	{
		return $_mimetypes[$mime_type];
	}
	
	return false;
	
}

{ToDo| [Proprietäre MIME-Types in die Funktion mime_to_ext() einbauen als include()] }


Bilder erkennen

Das Erkennen eines Bildes ist bei PHP relativ sicher durch die Funktion getimagesize() (Handbuch) möglich. Diese Funktion gehört zum Standardumfang von PHP, steht also auch zur Verfügung, wenn keine Grafik-Erweiterungen, wie z.B. GD-Lib oder ImageMagick installiert wurden.

Wird mittels getimagesize() ein Bildformat erkannt, kann dieses Bild allerdings immer noch schädlich für den Server sein. Es könnte

  • Schadcode enthalten (z.B. im EXIF-Header)
  • eine Dateiendung haben, die zum Parsen des Codes führt

Um ohne Angst Bilder auf den Server hochladen lassen zu können, sollte man daher beim Weiterverarbeiten auf dem Server die folgenden Regeln beachten:

  • nur Dateinamen (nebst Pfadprüfung bzw. Abtrennung mit basename(), Handbuch) mit Endungen aus einer Positivliste zulassen, besser noch, den Dateinamen auf dem Server generieren.
→ Siehe hierzu auch den Abschnitt Schädliche Dateinamen ausschließen
  • für Verzeichnisse, die hochgeladene Bilder direkt über HTTP(S) bereitstellen, sämtliche Interpreter, bin- und exe-Loader ausschalten, sodass durch die Files keinerlei Programmkontrolle erlangt werden kann
  • Für die abgelegten Dateien keinesfalls das Execute-Recht setzen (maximal rw-rw-r-- 664, rwxrwxrwx 777 ist böse!).
  • nur Bilder zulassen, also Prüfung mit getimagesize() (Handbuch)
  • Dateiendungen nach Prüfung mit getimagesize(), $_imagesize['mime'] (s. nachfolgend) oder get_mime_type() und mime_to_ext() richtigstellen

Die serverseitige Verwendung des vom Client gemeldeten MIME-Types in $_FILES['inputname']['type'] ist für die Absicherung nicht geeignet, da diese Angabe beliebig gefälscht sein kann.

Der Wert von getimagesize()

Beim Hochladen von Bildern sollte man alle möglichen Kontrollmechanismen verwenden. Bilder können als Hintertür missbraucht werden.

Mit der PHP-Funktion getimagesize() (Handbuch) kann man auf dem Server prüfen, welchen MIME-Type das hochgeladene Bild hat. Außerdem kann man weitere Metadaten des Bildes erfahren:

Beispiel
$_imagesize = getimagesize($_FILES['dateiupload']['tmp_name'], $_imageinfo);

Die Funktion gibt entweder ein Array zurück, das aber nicht immer alle Elemente enthalten muss

  • $_imagesize[0] => Breite des Bildes in Pixeln
  • $_imagesize[1] => Höhe des Bildes in Pixeln
  • $_imagesize[2] => Enthält einen numerischen Wert für den Bildtyp. (siehe Konstanten)
  • $_imagesize[3] => Enthält einen String für die Verwendug in HTML, leider für CSS nicht verwendbar
  • $_imagesize['mime'] => Enthält den MIME-Type des Bildes laut RFC
  • $_imagesize['channels'] => Enthält die Anzahl der Farbkanäle (also z.B. wieviel Bytes ein Pixel beansprucht)
  • $_imagesize['bits'] => Enthält die Anzahl der Bits pro Farbkanal

oder im Misserfolgsfall 'FALSE'.

Die Imagetype-Konstanten aus $_imagesize[2] lauten:

  • [IMAGETYPE_GIF] => 1
  • [IMAGETYPE_JPEG] => 2
  • [IMAGETYPE_PNG] => 3
  • [IMAGETYPE_SWF] => 4
  • [IMAGETYPE_PSD] => 5
  • [IMAGETYPE_BMP] => 6
  • [IMAGETYPE_TIFF_II] => 7
  • [IMAGETYPE_TIFF_MM] => 8
  • [IMAGETYPE_JPC] => 9
  • [IMAGETYPE_JP2] => 10
  • [IMAGETYPE_JPX] => 11
  • [IMAGETYPE_JB2] => 12
  • [IMAGETYPE_SWC] => 13
  • [IMAGETYPE_IFF] => 14
  • [IMAGETYPE_WBMP] => 15
  • [IMAGETYPE_JPEG2000] => 9
  • [IMAGETYPE_XBM] => 16

Es stehen also die Werte zu den zugehörigen Konstanten in $_imagesize[2].

Wird von der Funktion getimagesize() keiner der vorbenannten Typen erkannt, liefert sie false zurück.

Um aus dem Imagetype wieder eine File-Extension zu machen, schreiben wir uns wieder eine kleine Nachschlagefunktion:

Beispiel
## File-Extension aus dem Imagetype erzeugen

function get_img_file_ext($imagetype)
{
    if($imagetype === false) return false;

    $_types[IMAGETYPE_GIF]     = 'gif';
    $_types[IMAGETYPE_JPEG]    = 'jpg';
    $_types[IMAGETYPE_PNG]     = 'png';
    $_types[IMAGETYPE_SWF]     = 'swf';
    $_types[IMAGETYPE_PSD]     = 'psd';
    $_types[IMAGETYPE_BMP]     = 'bmp';
    $_types[IMAGETYPE_TIFF_II] = 'tif';
    $_types[IMAGETYPE_TIFF_MM] = 'tif';
    $_types[IMAGETYPE_JPC]     = 'jpc';
    $_types[IMAGETYPE_JP2]     = 'jp2';
    $_types[IMAGETYPE_JPX]     = 'jpx';
    $_types[IMAGETYPE_JB2]     = 'jb2';
    $_types[IMAGETYPE_SWC]     = 'swc';
    $_types[IMAGETYPE_IFF]     = 'iff';
    $_types[IMAGETYPE_WBMP]    = 'wbmp';
    $_types[IMAGETYPE_JPEG2000] = 'jpg';
    $_types[IMAGETYPE_XBM]     = 'xbm';

    if (isset($_types[$imagetype])) return $_types[$imagetype];

    return false;
}

Alternativ können wir die File-Extension auch mit Hilfe von $_imagesize['mime'] und der schon vorgestellten Funktion mime_to_ext() ermitteln.

Textdateien erkennen

{ToDo| [Reine Textdateien erkennen / Script-Ausschluss] }


Einfache Textformate

Unter Textdateien versteht man im Allgemeinen Dateien, die nur druckbare Zeichen enthalten nebst ein paar Druckersteuerzeichen und in der weiteren Entwicklung auch einigen Sonderzeichen unterschiedlicher (westeuropäischer) Sprachen. Grundsätzlich kann man erst einmal annehmen, dass alle Zeichen aus dem ASCII-Zeichensatz stammen müssen (weitere Ausführen siehe „höhere Textformate“).

Diese Dateien können reinen Klartext, HTML, Programm-Quellcode oder sonstige Informationen enthalten, die so gestaltet sind, dass sie mittels eines einfachen Texteditors für Menschen lesbar sind.

Betrachten wir zuerst reine Klartext-Dateien in ASCII-Codierung. Diese dürfen nur eine Untermenge des ASCII-Zeichensatzes enthalten, wenn sie in der Mehrzahl der Verarbeitungs- und Ausgabeumgebungen "unschädlich" sein sollen.

Keinesfalls dürfen sie aber den binären Wert 0 enthalten.


Code

dez.

Code

hexadez.

Zeichendar-

stellung

Bedeutung
0 00 0 NULL
1 01
2
3


Wenn wir durch Vergleich mit einer Positivliste sichergestellt haben, dass es sich bei der hochgeladenen Datei um eine reine ASCII-Textdatei (ggf. zuzüglich Sonderzeichen) handelt, müssen wir trotzdem noch sicherstellen, dass diese Datei kein ausführbares Programm oder Script enthält, das auf dem Server Schaden anrichten kann.

Eine weitere Schadenmöglichkeit besteht darin, dass diese Dateien Textsequenzen enthalten, die für den Client (hier den Browser des Betrachters) eine Steuerwirkung haben, die über die reine Druckersteuerung hinaus geht. Es könnten in einer Textdatei also zusätzliche Informationen im Format von

  • HTML
  • JavaScript
  • Java
  • Visual Basic for Applications
  • CSS (cascading style sheets)
  • ...

versteckt sein, die dann durch den Browser des Betrachters interpretiert werden. Lesen Sie hierzu am besten auch den Artikel: Programmiertechnik/Kontextwechsel.

Höhere Textformate
  • andere Codierungen
  • implizite Textformatierungen (z.B. RTF Rich Text Format)
  • ...

Schädliche Dateinamen ausschließen

Wenn Sie trotz der Warnungen einen vom Client gelieferten Dateinamen direkt zum Speichern der Datei auf dem Server verwenden wollen, müssen Sie unbedingt diverse Regeln beachten. Hier nochmals der Hinweis, dass eine Transformation in einer Datei oder Datenbanktabelle aus mehreren Gründen der bessere Weg ist!


Für Windows-Zielsysteme gilt

Verbotene Zeichen:

< > ? " : | \ / * NULL

NULL steht für ASCII #0.


Verbotene Dateinamen/Gerätebezeichnungen

CON, PRN, AUX, NUL,

COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9,

LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, und LPT9.

Für Linux-Zielsysteme gilt

Verbotene Zeichen:

/ NULL

NULL steht für ASCII #0.


Verbotene Dateinamen/Gerätebezeichnungen

Bei Linux/Unix werden alle Devices als Dateinamen über das Filesystem angesprochen. Wir müssen also verhindern, dass entsprechende Dateinamen erzeugt werden können. Auch alle empfindlichen Dateien sollten nach dem FHS (File System Hierarchie Standard) in tieferen Verzeichnisebenen liegen.

Beispiel
/dev/sda1
/etc/passwd
/usr/bin/useradd


Nicht schädlich fürs System, aber ganz gewiss für die Daten ist der Dateiname "/dev/null". Die Daten würden auf Nimmerwiedersehen verschwinden, wenn man versuchen würde, sie unter dieser Bezeichnung zu speichern.


Tilde Expansion ?

noch nicht weiter untersucht ...

.

Hochladen von Schlüsseln ?

noch nicht weiter untersucht ...

.

Für PHP gilt zusätzlich

Verbotene Dateinamen:

php://output ; php://input ; php://stdin ; php://stdout ; php://stderr

Für den Apache Webserver gilt

Verbotene Dateinamen:

.htaccess ## ist abhängig von der Serverkonfiguration!

Dateinamen anpassen

Wenn wir nun die Schnittmenge des Erlaubten für DOS/Windows und Linux bilden, bleibt eigentlich für einen noch lesbaren, aber zulässigen Dateinamen nur eine Untermenge der druckbaren ASCII-Zeichen übrig. Diese müssen wir aber noch weiter einschränken und zusätzliche Regeln kontrollieren.

  • Anfangs-Codierung sicherstellen
  • Umlaute entfernen bzw. ersetzen
  • kein führender Punkt, kein Punkt am Ende
  • keine Pfade zulassen ('\' und '/' entfernen bzw. austauschen)
  • alle nicht in einer Positivliste enthaltenen Zeichen ersetzen.
  • Dateiendungen (*.ext) kontrollieren und ggf. ersetzen oder ablehnen
  • Geschützte Dateinamen (Kanal- und Gerätenamen) verhindern
  • ggf. Case Un/Sensitivity berücksichtigen
  • ggf. auch Leerzeichen austauschen, obwohl diese in Dateinamen inzwischen erlaubt sind. Allerdings können wir den Schwierigkeiten in Referenzen (Links) hier bereits vorbeugen


Beispiel
function fname_filter( $s, $filter=true )
{
    ##@s: string, string to be filtered, must be encoded utf-8
    ##@filter: boolean, true->filter $s for use as filename and remove 
    ###   reserved expressions and signs for Windows, Linux, php, apache
   
    ## Normalizer-class missing!
    if (! class_exists("Normalizer", $autoload = false)) return false;
      
    ## maps German (umlauts) and other European characters onto two characters before just removing diacritics
    $s    = preg_replace( '@\x{00c4}@u'    , "Ae",    $s );    // umlaut Ä => Ae
    $s    = preg_replace( '@\x{00d6}@u'    , "Oe",    $s );    // umlaut Ö => Oe
    $s    = preg_replace( '@\x{00dc}@u'    , "Ue",    $s );    // umlaut Ü => Ue
    $s    = preg_replace( '@\x{00e4}@u'    , "ae",    $s );    // umlaut ä => ae
    $s    = preg_replace( '@\x{00f6}@u'    , "oe",    $s );    // umlaut ö => oe
    $s    = preg_replace( '@\x{00fc}@u'    , "ue",    $s );    // umlaut ü => ue
    $s    = preg_replace( '@\x{00f1}@u'    , "ny",    $s );    // ñ => ny
    $s    = preg_replace( '@\x{00ff}@u'    , "yu",    $s );    // ÿ => yu
    
    ## maps special characters (characters with diacritics) on their base-character followed by the diacritical mark
    ## exmaple:  Ú => U´,  á => a`
    $s    = Normalizer::normalize( $s, Normalizer::FORM_D );
   
    $s    = preg_replace( '@\pM@u'        , "",    $s );    // removes diacritics
   
    $s    = preg_replace( '@\x{00df}@u'    , "ss",    $s );    // maps German ß onto ss
    $s    = preg_replace( '@\x{00c6}@u'    , "Ae",    $s );    // Æ => Ae
    $s    = preg_replace( '@\x{00e6}@u'    , "ae",    $s );    // æ => ae
    $s    = preg_replace( '@\x{0132}@u'    , "Ij",    $s );    // ? => Ij
    $s    = preg_replace( '@\x{0133}@u'    , "ij",    $s );    // ? => ij
    $s    = preg_replace( '@\x{0152}@u'    , "Oe",    $s );    // Π=> Oe
    $s    = preg_replace( '@\x{0153}@u'    , "oe",    $s );    // œ => oe
   
    $s    = preg_replace( '@\x{00d0}@u'    , "D",    $s );    // Ð => D
    $s    = preg_replace( '@\x{0110}@u'    , "D",    $s );    // Ð => D
    $s    = preg_replace( '@\x{00f0}@u'    , "d",    $s );    // ð => d
    $s    = preg_replace( '@\x{0111}@u'    , "d",    $s );    // d => d
    $s    = preg_replace( '@\x{0126}@u'    , "H",    $s );    // H => H
    $s    = preg_replace( '@\x{0127}@u'    , "h",    $s );    // h => h
    $s    = preg_replace( '@\x{0131}@u'    , "i",    $s );    // i => i
    $s    = preg_replace( '@\x{0138}@u'    , "k",    $s );    // ? => k
    $s    = preg_replace( '@\x{013f}@u'    , "L",    $s );    // ? => L
    $s    = preg_replace( '@\x{0141}@u'    , "L",    $s );    // L => L
    $s    = preg_replace( '@\x{0140}@u'    , "l",    $s );    // ? => l
    $s    = preg_replace( '@\x{0142}@u'    , "l",    $s );    // l => l
    $s    = preg_replace( '@\x{014a}@u'    , "N",    $s );    // ? => N
    $s    = preg_replace( '@\x{0149}@u'    , "n",    $s );    // ? => n
    $s    = preg_replace( '@\x{014b}@u'    , "n",    $s );    // ? => n
    $s    = preg_replace( '@\x{00d8}@u'    , "O",    $s );    // Ø => O
    $s    = preg_replace( '@\x{00f8}@u'    , "o",    $s );    // ø => o
    $s    = preg_replace( '@\x{017f}@u'    , "s",    $s );    // ? => s
    $s    = preg_replace( '@\x{00de}@u'    , "T",    $s );    // Þ => T
    $s    = preg_replace( '@\x{0166}@u'    , "T",    $s );    // T => T
    $s    = preg_replace( '@\x{00fe}@u'    , "t",    $s );    // þ => t
    $s    = preg_replace( '@\x{0167}@u'    , "t",    $s );    // t => t
   
    // remove all non-ASCii characters
    $s    = preg_replace( '@[^\0-\x80]@u'    , "",    $s );

    ## remove all reserved expressions and signs for Windows, Linux, php, apache
    if ($filter === true)
    {
	$_search = array(' ', 0x0, '<', '>', '|', '?', '"', ':', '\\', '/', '*'); 
	
	$s = str_replace( $_search, '_', $s ); 
		
        ## remove leading and trailing dot
	$s = trim($s, '.');

	## avoid reserved expressions
	$_names = array( 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 
	  'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 
	  'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'); 

	if (in_array(strtoupper($s), $_names, true)) return false;   
    }
	
    ## possible errors in UTF8-regular-expressions
    if (empty($s)) return false;

    return $s;
}

Scripte verhindern

Wenn das PHP-System als Modul im Apachen benutzt wird, sollte man für das per HTTP/s erreichbare Bilderverzeichnis den Parser ausschalten. Dies geht z.B. per Eintrag in der .htaccess-Datei des Verzeichnisses:

  'php_flag engine 0'

oder auch einem Eintrag in der passenden Directory-Divison der Virtual-Host-Konfiguration, was sicherer ist

  'php_admin_flag engine 0'      ## Überschreiben in der .htaccess-Datei ist verboten

oder

  'php_flag engine 0'            ## Überschreiben in der .htaccess-Datei ist erlaubt 


Hierdurch wäre dann die Interpretation und Ausführung von PHP-Dateien, die in dem betroffenen Verzeichnis liegen, unterdrückt.

Eine zusätzliche Maßnahme, um die Ausführung von Scripten zu verhindern, ist die Wahl einer ungefährlichen Dateiendung. Wird PHP als Modul des Webservers eingesetzt, entscheiden die Dateiendungen darüber, ob abgerufene Ressourcen (hier Dateien) geparst werden, oder nicht.

Diese Einstellung ist aber von der Server-Konfiguration abhängig und könnte sich durch Eingriffe des Server-Administrators ändern!

Typischerweise werden z.B. Dateien mit den Endungen *.jpg, *.png, *.gif, *.bmp usw. nicht geparst. Bei der Verschiebung von Bildern kann man also die tatsächlich dafür vorgesehene Dateieindung aus einer Positivliste für die Zieldatei benutzten. Mehr darüber bei "Bilder erkennen".

Es könnten aber auch andere Script-Interpreter aktiv sein, die man eventuell gar nicht benutzt. Bei Standardeinrichtungen sind auch Perl-Scripte vorgesehen. Den Fokus nur auf PHP zu richten, auch wenn man selbst nur mit PHP arbeitet, ist daher falsch.

Sichern von Uploads

Konfigurationseinstellungen

Auf den Upload von Massendaten wirken sich unterschiedliche Konfigurationseinstellungen (php.ini) aus:

  • Serverbeschränkungen (z.B. bei Apache LimitRequestBody, Handbuch)

Außerdem müssen (je nach verwendetem PHP-Interpretersystem: CGI, Fast-CGI, Modul) die Einstellungen stimmen für


Kontrolliert speichern

Eine rudimentäre Funktion, hochgeladene Daten nach den erfolgten notwendigen Tests aus dem Temporärverzeichnis an ihren persistenten Speicherort zu verschieben, stellt PHP mit

move_uploaded_file() zur Verfügung.

Obwohl laut POSIX-Standard Streams bei Benutzung gesperrt werden müssen (advisory Lock), konnte ich dies bei dieser Funktion weder auf der Quellseite noch auf der Zielseite feststellen. Außerdem überschreibt die Funktion ohne Warnung bereits vorhandene Files.

Der eigentlich beabsichtigte Nutzen für diese Funktion, nämlich nur die von der eigenen Scriptinstanz als hochgeladen entgegengenommenen Dateien verschieben zu können, wird durch die Einführung von $_FILES auch obsolet. Der INHALT der "hochgeladenen" Datei wurde ohnehin nie abgesichert. Es wäre also immer noch möglich gewesen, den INHALT der Datei während der Zwischenspeicherung im Temporärverzeichnis durch eine Fremdinstanz zu verändern. Dies wird auch durch setzen des t-Flags im Dateisystem (Linux) nicht verhindert.

Da PHP aber auch als Modul des Apachen die Möglichkeit bietet, jeder Domain (jeden VirtHost) ein eigenes Temporärverzeichnis zur Verfügung zu stellen, auf das (bei richtiger Einrichtung des Webservers) andere VirtHosts ("Domains") mittels PHP keinen Zugriff haben, ist das Problem der Veränderung des INHALTS durch PHP-Scripte beseitigt. Andere, parallel vorhandene Installationen mit anderen Sprachen sind hiermit aber leider noch nicht abgesichert!

Durch Einführung von $_FILES mit den sicheren Elementen

  • $_FILES[$inputname]['tmp_name'],
  • $_FILES[$inputname]['error'] und
  • $_FILES[$inputname]['size']

können wir uns nun selber eine sichere Funktion erstellen, die sowohl auf der Quellseite, als auch auf der Zielseite "weiß, was sie tut", also das atomistische Modell einhält und uns kein TOCTTOU-Problem baut. Siehe auch https://de.wikipedia.org/wiki/Time-of-Check-to-Time-of-Use-Problem.

Wir haben also mehrere Freiheitsgrade:

  • Datei auf der Quellseite ist nicht vorhanden -> Abbruch
  • Datei auf der Quellseite ist vorhanden -> Öffnen und zum Lesen sperren
  • Datei auf der Zielseite soll vorhanden sein und soll überschrieben werden -> öffnen, sperren, überschreiben, kürzen
  • Datei auf der Zielseite darf nicht vorhanden sein und muss neu angelegt werden -> anlegen, sperren, beschreiben
  • Datei auf der Zielseite soll auf jeden Fall geschrieben werden -> Neu anlegen oder öffnen, sperren, beschreiben/überschreiben

Diese Liste und ihre Umsetzung ist jeweils abhängig vom Filesystem und der verwendeten Programmiersprache, in der die Funktionen auf die API zugreifen. Das muss uns hier im Moment aber nicht weiter interessieren.

Beispiel
#---- Error Codes ---Samples------
#  0: No Error
#---- Low Level Errors -----------
#  1:
#  2: File not Found
#  3: File already exists
#  4: Could not read file, no data
#  5: Could not open file
#  6: Could not lock file 
#  7:
#---- Data Errors ----------------
# 11: No valid data 
# 12: No valid Attribs
# 99: unknown Error


Beispiel
function save_uploaded_file($source, $target, $overwrite=0)
{
    # $overwrite == 0 -> no overwrite, only create if not exists
    # $overwrite == 1 -> open only if exists, overwrite, truncate!
    # $overwrite == 2 -> rewrite existing or create new

    # we believe that php will not produce 'lost handles' if we leave 
    # this function without closing them.

    if (!is_int($overwrite)) return 12;

    # open source
    if (!$fs = fopen($source, 'rb')) return 5;
    if (!flock($fs, LOCK_SH)) return 6; ## Source could not be locked;

    # open target

    switch ($overwrite) 
    {
        case 0:  
            if (!$ft = fopen($target, 'xb'))  return 3;  ## assumed 'already exists'
        break;

        case 1:
            if (!$ft = fopen($target, 'rb+')) return 2;  ## assumed 'not found'
        break;

        case 2:
            if (!$ft = fopen($target, 'wb'))  return 5;  ## assumed 'could not open'
        break;

        default:
            return 12;
    }

    if (!flock($ft, LOCK_EX)) return 6;          ## Target could not be locked;

    $filesize = filesize($source);
    $cont = fread($fs, $filesize);
    if (strlen($cont) != $filesize) return 4;    

    fwrite($ft, $cont);

    fclose($fs);

    ## new file perhaps is shorter than old one
    ftruncate($ft, $filesize);
    fclose($ft);
     
    return 0;

}

Dateien bereitstellen

innerhalb der Document Root

Script-Ausführung abschalten

Wenn keine andere Möglichkeit besteht, oder man es sich "einfach" machen will (z. B. für öffentliche Bilderverzeichnisse), dann speichert man die hochgeladenen Dateien i.d.R. in einem Verzeichnis, das innerhalb der Document-Root liegt. Das führt dann normalerweise auch dazu, dass diverse Dateien (Dateiendungen) zur Ausführung (Interpretation) der hochgeladenen Datei führen, wenn diese per http/s aufgerufen wird.

Um zumindest zu verhindern, dass PHP-Dateien in diesem Verzeichnis ausgeführt werden können, sollte man unbedingt den Parser für dieses Verzeichnis abschalten.

Beispiel
# .htaccess ## für PHP als Apache-Modul

php_value engine 0

# end of .htaccess

Dass die .htaccess-Datei nun besonders geschützt sein muss, also sicherheitshalber nur durch root beschreibbar sein darf, versteht sich hoffentlich von selbst.

Besser ist es ohnehin, diese Einstellung in den VirtHost-Konfiguration oder der httpd.conf vorzunehmen, dort dann auch am besten als Admin-User

php_admin_value engine 0

Beispiel
## .htaccess ## für PHP als CGI- oder FastCGI-Script
## https://httpd.apache.org/docs/2.4/de/mod/mod_mime.html#removehandler 

RemoveHandler .php .php4 .php5 .phps ### und so weiter

# end of .htaccess

Hierzu sollte man möglichst wissen, für welche Dateiendungen PHP-Handler eingerichtet sind, damit keine Möglichkeit durchrutscht. Diese Apache-Direktive gilt immer für das Verzeichnis, ab dem sie eingerichtet ist, wird also in tiefere Verzeichnisebenen vererbt.

Hochladen schädlicher Dateien unterbinden

Im Sicherheitskonzept der "Uploadspezialisten", auch in etablierten CMS, wird immer gerne vergessen, dass es außer ausführbaren Dateien auch eine nichtausführbare gibt, mit der man sehr viel Schaden anrichten kann.

die Datei .htaccess [Die Bezeichnung ist außerdem abhängig von den Servereinstellungen]

Gelingt es einem Client, eine eigene .htaccess auf einem Apache-Webserver innerhalb der Document-Root zu platzieren, kann er damit meistens den Server übernehmen.

außerhalb der Document Root

siehe vorerst unter hochgeladene Inhalte bereitstellen

in Datenbanken speichern

Typische Sicherheitslücken

{ToDo| [Übersicht über die üblichen Sicherheitslücken] }

Workflow

Schritt für Schritt zum Ziel

  • Testserver in der DMZ (LAN) einrichten
  • VirtHost einrichten
  • Verzeichnisse laut Abschnitt Verzeichnisstruktur einrichten
  • Einstellungen in den Servereinstellungen und den VirtHosts anpassen,
 Server neu starten und testen
  • Formular in HTML erstellen für den Client
  • Testscript in PHP erstellen, mit dem $_FILES ausgewertet wird
  • Kontrollen durchführen
  • Script (Funktionen) erstellen für den Upload
  • Uploaded File nach allen Regeln der Kunst auf schädliche Inhalte prüfen
  • Uploaded File sicher ins zutreffende Repository übertragen und ggf. den zugehörigen Eintrag in der Datenbank erstellen
  • Dem User eine aussagefähige Quittung senden
  • Zugriffsrechte regeln
  • Download-Angebote bereitstellen (unter Berücksichtigung der jeweiligen User-Rechte)
  • Sicherheitslücken suchen, Grenzen ermitteln
  • Webangebot aus der DMZ auf den Produktivserver übertragen
  • Testen, testen, testen

Dialog-Elemente und File-Upload kombinieren

In einem Formular können sich diverse unterschiedliche Elemente und Input-Elemente diverser Typen befinden:

  • Text
  • Select
  • Radio
  • Hidden
  • Textarea

.

  • File


Es ist meistens nicht sinnvoll, die Elemente vom Typ File sofort zusammen mit den textbasierten Elementen zu übertragen. Elemente vom Typ File haben meistens ein großes Transfervolumen und außerdem müssen sie zugeordnet werden können. Wenn die Zuordnung aber erst durch die ggf. fehlerhaften textbasierten Elemente stattfinden kann, hängen die Files auf dem Server "in der Luft". Wenn die Request-Abarbeitung endet, gehen sie üblicherweise wieder verloren und müssten jedes Mal erneut übertragen werden.

Eine mehrstufige Übertragung ist daher sinnvoll. Zuerst werden die textbasierten Elemente übertragen und solange geprüft, bis sie akzeptiert werden können und erst dann werden durch das Form die File-Elemente zum Upload angefordert.


{ToDo| [Mehrstufiges Affenformular mit history.back()-Sperre]

Fileupload wird erst freigegeben, wenn die einfachen Textfelder akzeptiert werden konnten

  • Arbeiten mit serialized Forwards
  • Arbeiten mit Sessions
  • Einsatz von header

}

Hauptartikel: PHP/Tutorials/Reloadsperre

Sessions nutzen für den File-Upload

Multiupload per JavaScript unterstützen

Es gibt die Möglichkeit, mit JavaScript Dateien in Teilen hochzuladen, die dann von einem serverseitigen Script wieder zusammengefügt werden müssen. Diese Vorgehensweise ist besonders dann sinnvoll, wenn man auf der einen Seite eine Größenbeschränkung für Uploads hat, und wenn man beim Hochladen mehrerer Dateien einen gewissen Komfort haben möchte.

Die schwedische Firma Moxiecode hat ein JavaScript-Werkzeug erstellt, mit dem sich das Hochladen von Dateien sehr komfortabel auch in kleinen Datenhäppchen umsetzen lässt: Plupload. Im Zusammenspiel mit einem PHP-Script soll hier gezeigt werden, wie man mit Plupload einen Uploader bauen kann:

Uploader mit Plupload und PHP
<?php
/**
 * function to handle a file upload via Plupload UI widget
 * (http://plupload.com)
 *
 * This function also checks the $_POST array for two more strings:
 * 'chunk' (current number, 0-based) and
 * 'chunks' (total number of chunks).
 *
 * @param string key for the $_FILES array
 * @param string relative file path
 * @param string allowed file type ('image', 'html' or default '*' for any type)
 */
function handle_file_upload ($key, $file, $do_backup = true, $allowed_type = '*') {

    $backup = $file;

    // new filename for already existing file (and older copies)?
    if ($do_backup && is_file($file)) {

        while (is_file($backup)) {

            // add '_old' before file type extension
            $backup = preg_replace(
                '~(\.[^.]+)$~',
                '_old$1',
                $backup
            );
        }
    }

    if (array_key_exists($key, $_FILES)
        && $_FILES[$key]['error'] === 0
    ) {

        /* let's presume we either got the last chunk or
         * a one-chunk upload */
        $ready = true;

        if (array_key_exists('chunk', $_POST)
            && array_key_exists('chunks', $_POST)
        ) {

            // first chunk?
            if ($_POST['chunk'] < 1) {

                // avoid overwriting of existing files
                if ($backup !== $file) {
                    rename($file, $backup);
                }

                // start a new file
                $fh = fopen($file, 'wb+');

            } else {

                $backup = $file;

                // continue partial file
                $fh = fopen($file, 'ab+');
            }

            // not last chunk?
            if ($_POST['chunk'] + 1 < $_POST['chunks']) {
                $ready = false;
            }

        } else {

            // avoid overwriting of existing files
            if ($backup !== $file) {
                rename($file, $backup);
            }

            // start a new file
            $fh = fopen($file, 'wb+');
        }

        // copy file data from temp file
        $tmp = fopen($_FILES[$key]['tmp_name'], 'rb');

        while ($chunk = fread($tmp, 1024*1024)) {
            fwrite($fh, $chunk);
        }

        fclose($tmp);
        fclose($fh);

        // test for allowed file type
        if ($ready) {

            $ok = ($allowed_type == '*');

            switch ($allowed_type) {

                case 'image':
                    $tmp = getimagesize($file);

                    $ok = (
                        // width > 0px?
                        $tmp[0] > 0
                        // height > 0px?
                        && $tmp[1] > 0
                        // extension OK?
                        && preg_match(
                            '~(?i)\.(gif|jpeg|jpg|png)$~',
                            $file
                        )
                    );
                break;

                case 'html':
                    $ok = (
                        // is data HTML code?
                        preg_match(
                            '~(?is)<html[^>]*>.*</html>~',
                            file_get_contents($file)
                        )
                        // filename extension OK?
                        && preg_match('~(?i)\.html?$~', $file)
                    );
                break;
            }

            if (!$ok) {

                // delete uploaded file
                unlink($file);

                // restore old file name on backup?
                if ($backup !== $file) {
                    rename($backup, $file);
                }
            }
        }
    }
}

/**
 * we define 'upload-file' as the name for the POST parameter
 */
$key = 'upload-file';

/**
 * deal with upload?
 */
if (array_key_exists($key, $_FILES)) {

    handle_file_upload(
        // 'upload-file' for $_POST['upload-file'] and $_FILES['upload-file']
        $key,
        // filename and path for storage
        sprintf(
            './uploaded/%s',
            // remove any unwanted path info from file name
            preg_replace('~^.*(/|\\\\)~', '', $_FILES[$key]['name'])
        ),
        // yes, please rename existing file first
        true,
        // accept both HTML and image files
        '*'
    );
}

/**
 * Now the HTML document
 */
?>
<!DOCTYPE html>
<html>
    <head>
        <title>File Uploader</title>
        <script src="./plupload/plupload.full.min.js" type="text/javascript"></script>
    </head>
    <body>
        <h1>File Uploader</h1>
        <p>This uploads both HTML and image files.</p>
        <div id="filelist">Your browser doesn't have Flash, Silverlight or HTML5 support.</div>
        <p id="container">
            <a id="pickfiles" href="javascript:;">[Select files]</a>
            <a id="uploadfiles" href="javascript:;">[Upload files]</a>
        </p>
        <pre id="console"></pre>
        <script type="text/javascript">
//<![CDATA[
new function () {
    // see http://plupload.com/examples/core for this code
    var uploader = new plupload.Uploader({
        runtimes : 'html5,flash,silverlight,html4',

        browse_button : "pickfiles", // you can pass in id...
        container: document.getElementById("container"), // ... or DOM Element itself

        file_data_name : "<?php echo $key; ?>",
        url : "<?php echo $_SERVER['SCRIPT_NAME']; ?>",

        filters : {
            max_file_size : "2mb",
            mime_types: [
                {title : "Image files", extensions : "jpeg,jpg,gif,png"},
                {title : "HTML files", extensions : "htm,html"}
            ]
        },

        // Flash settings
        flash_swf_url : "./plupload/js/Moxie.swf",

        // Silverlight settings
        silverlight_xap_url : "./plupload/js/Moxie.xap",


        init: {
            PostInit: function() {
                document.getElementById("filelist").innerHTML = '';

                document.getElementById("uploadfiles").onclick = function() {
                    uploader.start();
                    return false;
                };
            },

            FilesAdded: function(up, files) {
                plupload.each(files, function(file) {
                    document.getElementById("filelist").innerHTML += '<div id="'
                        + file.id
                        + '">'
                        + file.name
                        + " ("
                        + plupload.formatSize(file.size)
                        + ") <b></b></div>";
                });
            },

            UploadProgress: function(up, file) {
                document.getElementById(file.id).getElementsByTagName("b")[0].innerHTML = "<span>"
                    + file.percent
                    + "%</span>";
            },

            Error: function(up, err) {
                document.getElementById("console").innerHTML += "\nError #" + err.code + ": " + err.message;
            }
        }
    });

    uploader.init();
}
        //]]></script>
    </body>
</html>

Damit das obige PHP-Script auf einem Testsystem funktioniert, benötigt man folgende Dinge:

  • Plupload-Dateien entpackt in einem Unterverzeichnis "plupload" (Bezugsquelle Plupload.com)
  • ein Unterverzeichnis "uploaded" für die hochgeladenen Dateien
  • obiges Script als PHP-Datei im selben Verzeichnis wie die beiden Unterverzeichnisse

die PHP-Logik

Das PHP-Script definiert einen Schlüssel mit dem Wert "upload-file", mit dem die Downloads an den Server geposted werden müssen, da sie sonst nicht angenommen werden. Dieser Schlüssel findet sich auch im JavaScript-Code wieder, wo er von PHP bei der Auslieferung des HTML-Dokuments hineingeschrieben wird.

Die PHP-Funktion handle_file_upload regelt das Entgegennehmen der jeweils hochgeladenen Datei bzw. des aktuellen Fragments. Dabei achtet sie darauf, ob eine bereits vorhandene Datei gleichen Namens vorher umbenannt werden soll und setzt gegebenenfalls ein Suffix an den Dateinamen. Außerdem führt sie eine Prüfung durch, ob die hochgeladene Datei eine gültige Bilddatei oder ein gültiges HTML-Dokument ist, und ob eine Datei dieses Typs entgegengenommen werden darf. Im Zweifelsfalle löscht sie die Datei wieder. Prüfungen auf andere Dateitypen sind zwar denkbar, hier jedoch nicht umgesetzt. Sie lassen sich an entsprechender Stelle aber nachrüsten.

Durch das Senden von Fragmenten lässt sich die Prüfung auf einen gültigen Dateityp nur nach dem vollständigen Hochladen sinnvoll durchführen. Daher müssen die Fragmente aus dem temporären Verzeichnis in den eigentlichen Zielordner gespeichert und dort zusammengesetzt werden. Ein anderes Verzeichnis des Webprojektes, welches durch eine Zugriffsbeschränkung von außen nicht erreichbar ist, wäre als Zwischenspeicher vor der endgültigen Prüfung denkbar, ist im obigen Beispiel aber nicht umgesetzt, da solche Dinge je nach Projekt sehr unterschiedlich zu lösen sind.

die JavaScript-Logik mit Plupload

Das Plupload-Objekt kennt eine Menge an Einstellungsmöglichkeiten, unter anderem den Schlüssel, mit dem die Daten binär versendet werden sollen (in unserem Beispiel "upload-file"). Dieser Schlüssel ist über die Eigenschaft file_data_name definierbar und wird von PHP dort auch eingetragen. Auch die Zieladresse trägt PHP in der Eigenschaft URL ein - nämlich die Adresse des aktuell laufenden PHP-Scripts.

Plupload zerlegt nun die aktuell hochzuladende Datei in Häppchen zu 2MB (siehe Eigenschaft filters.max_file_size). Je nach Beschränkung in den PHP-Einstellungen des eigenen Webprojekts muss dieser Wert angepasst werden, da das hochgeladene Fragment von PHP ansonsten verworfen und der Upload insgesamt scheitern würde, wenn zu viele Daten auf einmal geposted würden.

Für die Auswahl der hochzuladenden Dateien und für den Upload-Vorgang an sich kann über die Konfiguration filters.mime_types eine Liste an zulässigen Dateitypen definiert werden. Diese Beschränkung macht aber das Überprüfen auf gültige Dateitypen im PHP-Code keinesfalls überflüssig!

Die restlichen Konfigurationsmöglichkeiten regeln das Handling innerhalb des HTML-Dokuments, wie z.B. die Liste der Dateien, die auf das Hochladen warten, und wieviel Prozent davon schon hochgeladen wurden. Auch was die Darstellung angeht, bietet Plupload eine ganze Reihe von weiteren Möglichkeiten.

Anders als bei "herkömmlichen" Upload-Formularen wird hier das HTML-Dokument nicht mehr neu geladen, nachdem eine Datei hochgeladen wurde. Stattdessen erhält man die Rückmeldung über JavaScript ins aktuelle Dokument eingeblendet. Aus diesem Grund braucht es im HTML-Dokument überhaupt kein Formular mehr. Plupload bedient sich einfacher HTML-Elemente wie <div>, <a> und <pre>, um sie mit Funktionalität zu erweitern. Andere Block- und Inline-Elemente sind ebenso geeignet, Plupload benötigt in seiner Konfiguration lediglich die ID des jeweiligen Elements.

Hochgeladene Inhalte gesichert bereitstellen

URL-Parameter contra URL-Rewriting (mod_rewrite)

Bilder ausliefern per Script

Wir haben bereits erwähnt, dass es eine Methode sein kann, hochgeladene Bilder oder Inhalte eines Users vor dem Zugriff anderer User zu schützen, in dem man sie auf dem Web-Host in einem Verzeichnis speichert, das nicht direkt per HTTP/s zugänglich ist. Damit kann der Browser des entfernten Client nicht ungeprüft (per HTTP/s) auf die Ressource zugreifen.

Um die Bilder trotzdem ausliefern zu können, benötigt man dann ein Script. Dessen URI wird im Frontend ganz einfach dort, wo sonst die URL auf das Bild eingebunden wird, eingebunden.

Bevor nun das Bild mit der Funktion ausgegeben wird, muss jetzt das Programm (Script) prüfen, ob der User berechtigt ist, die angeforderte Ressource zu erhalten. Dies geht am besten per internem Check auf "Login/User-ID" und Verwendung einer Datenbanktabelle.


Beispiel
## Zugehörige Konstanten ##
define ('CACHE_MUST_REVALIDATE', 604800);   ## Zeit, nach der der Cache die Verifikation betreiben soll

Funktion für die Image-Response:

Beispiel
function put_image($filename, $aliasname = false)
{
    #@filename    String   Pfad zur Datei im lokalen Dateisystem
    #@aliasname   String   Name, der vom Client zum Speichern vorgeschlagen werden soll
    #                      wenn er angebeben wurde, wird der Name mit übertragen

    if (headers_sent()) return false;

    $fh = @fopen($filename,"rb");  
    if (!$fh) return false;
    if (!flock($fh, LOCK_SH)) return false; 
  
    ### prüfen, ob das Bild im Server neuer ist als im Cache des Browsers ###
    $last_modified = @gmdate('D, d M Y H:i:s',@filemtime($filename)).' GMT';

    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) 
    {
        // parse header
        $if_modified_since = preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
  
        if ($if_modified_since == $last_modified) 
        {
            // the browser's cache is still up to date
            header("HTTP/1.0 304 Not Modified");
            header("Cache-Control: max-age=" . CACHE_MUST_REVALIDATE . ", must-revalidate");
            fclose($fh);
            return true;
        }   
    }

    if (!$_imgsize = @getimagesize($filename))
    {
        fclose($fh);
        return false;
    }   

    if (isset($_imgsize['mime']))
    {   
        header ("Last-Modified: $last_modified"); 
        header("Content-type: {$_imgsize['mime']}");

        if (false !== $aliasname)
	{
	    header('Content-Disposition: inline; filename="' . $aliasname . '"');
	}	

        fpassthru($fh);
        fclose($fh);
        return true; 
    }
  
    fclose($fh);
    return false;
}

Man muss darauf achten, dass das Script, in dem diese Funktion benutzt wird, keinerlei weitere Ausgaben vornimmt, wenn das Bild erfolgreich ausgegeben werden konnte.


Das Script für den Einsatz z. B. im <img>-Element kann nach dem folgenden Muster aufgebaut werden:

Beispiel
<?php  ### getimg.php ### Bild aus dem Filesystem ausliefern ### utf-8 ### ÄÖÜäöü
    
    ## GET-Parameter mit Bild-Nummer vorhanden?
    
    ## Benutzer angemeldet?

    ## Datenbankverbindung vorhanden?

    ## Abfrage: Darf der User dieses Bild sehen?
    ##          wie heißt das Bild mit Trivialnamen?

    ##-> Ja: Bild ausliefern mit put_image()
    ##-> Nein: Ersatzbild ausliefern

    exit (0);
?>

Das fertige Script speichern wir dann in der Document-Root.


Im HTML-Code wird das dann einfach genauso, wie eine Bild-URL eingebunden:

Beispiel
    <img class="mystyle" src="/getimg.php?n=101356" alt="Bild vom Geburtstag" title="3 x 0 = 30">

Wie wir sehen, können sowohl im ALT-Attribut als auch im TITLE-Attribut bereits sensible Daten stehen. Daher sollte in einem CMS dem Client der gesamte Image-Tag gar nicht erst angeboten werden, wenn der Client keine Rechte auf das Source-Objekt und dessen Beschreibung hat.

Trotzdem muss der Request /getimg.php?n=101356 separat abgesichert bleiben, da er auch diskret (also ohne das CMS) ausgeführt werden kann!

Files bereitstellen per Script

Dass man einem Skript per URL-Parametern mitteilen kann, welche Ressourcen es heranziehen soll für die Beantwortung der Anfrage, haben wir im letzten Kapitel gesehen. Wäre aber nach außen nicht viel schöner, den direkten Aufruf einer Ressource mit einem sprechenden Bezeichner zu benutzen und trotzdem das Skript zur Kontrolle des Requests zwischengeschaltet zu haben?

[Beispiel mit URL-Parametern]

[Beispiel mit sprechendem Ressoource-Path]

Wie funktioniert das?

Unkontrollierter Zugriff

Bedingter Zugriff

Bedingter Zugriff mit spezifischen Benutzerrechten

Kontrolle per Sessiondaten contra Datenbankabfrage

{ToDo|... to be written Das will ich gerne übernehmen, denn das habe ich gerade durchexerziert

Robert Roth --93.195.228.97 10:04, 20. Dez. 2014 (CET)}

Large Objects aus der Datenbank bereitstellen

{ToDo|... to be written}

Quellen

  1. Self-Forum: PHP-gesteuerter File-Upload als POST-Request... und dann 413 vom 16.10.2016