Problembeschreibung ZIP-Upload per MediaManager

Für einen Importfilter innerhalb eine Joomla-Projektes wollte ich dem Kunden über den MediaManager die Möglichkeit zur Verfügung stellen, ZIP-Archive hochzuladen. Dabei zeigte sich ein fast unlösbares Problem. Während z.B. Archive vom Typ GZ oder 7Z unproblematisch hochgeladen werden konnten, gelang das mit unter Windows erstellten ZIP-Archiven einfach nicht. Kontinuierlich erhielt ich beim Uploadversuch die Fehlermeldung:

Fehler

Es wurde versucht eine Datei bzw. mehrere unsichere Dateien hochzuladen.

 

Ursachensuche und gescheiterte Workarounds

Die Ursachensuche gestaltete sich aufwändig. Natürlich hatte ich zunächst vermutet, dass es an den Optionen des MediaManagers liegt. Deshalb hatte ich in die Erlaubten Dateiendungen zip,ZIP, etc. hinzugefügt. Auch in die zu ignorierenden Dateiendungen ZIP,zip einzutragen war keine Lösung. In das Feld Erlaubte Dateitypen (MIME): habe ich dann noch probiert die Liste zu erweitern durch folgende Einträge: application/x-zip,application/x-gzip,application/zip,application/x-7z-compressed,application/x-zip-compressed,application/octed-stream.
Das sind zwar alles Maßnahmen, die nicht unwichtig sind, damit es überhaupt funktionieren kann, aber der Fehler blieb.

 

 

Viele Foreneinträge beschäftigen sich ebenfalls mit diesem Problem, da auch andere Komponentenentwickler wie z.B. Phoca oder Seblod damit beim Upload zu kämpfen hatten - im Prinzip alle, die die Joomla-Libraries zum Dateiupload JFile::upload(); nutzen. Klar steht als Grund fest, dass Joomla hier seit einigen Versionen eine Filterung eingebaut hat, die die Aufgabe hat, zu verhindern, dass unsichere Dateien auf den Server übertragen werden. Dabei wird nicht nur die Datei selbst angeschaut, sondern auch der Inhalt von ZIP-Archiven. Warum gerade ZIP-Archive dabei im Gegensatz zu z.B. GZ-Archiven so strikt behandelt werden, ist unklar und nicht nachvollziehbar.

Ein weiterer Hinweis, den ich probiert hatte, bestand darin im Core-Script administrator/components/com_media/controllers/file.json.php in ca. Zeile 198 den o.g. Joomla-Upload-Methodenaufruf if (!JFile::upload($object_file->tmp_name, $object_file->filepath)) { ... } zu verändern. Neben den hier zwei sichtbaren Argumenten können weitere übergeben werden. Der 4. mögliche Parameter allow_unsafe kann auf true gesetzt werden. Damit würden diverse Sicherheitsprüfungen unterbunden. Das sollte man natürlich nur unter bestimmten Voraussetzungen praktizieren, denn damit würden Sicherheitsmechanismen von Joomla an dieser Stelle außer Kraft gesetzt. Aber unabhängig davon, hat auch diese Änderung der Zeile in dieser Form den o.g. Fehler nicht verhindern können:

if (!JFile::upload($object_file->tmp_name, $object_file->filepath, $use_streams = false, $allow_unsafe = true)) { ... }

Die Suche ging also weiter.



 

Die Lösung - Änderung des Filtertyps

Ich habe mich dann in weitere Joomla-Core-Script vertieft. Zunächst suchte ich nach der Sprachconstanten für o.g. Fehlermeldung. Diese lautet COM_MEDIA_ERROR_WARNFILENOTSAFE. Daraufhin fand ich, dass im Script: administrator/components/com_mdsktools/controllers/file.php in der upload-Methode ca. Zeile 65 die Prüfung der Variablen $files diese Fehlermeldung wirft und die Scriptausführung mit einer false-Rückgabe abbricht. $files sollte eigentlich ein gefülltes Array sein und wird nur wenige Zeilen oberhalb durch diese Zeile gefüllt:

$files = $this->input->files->get('Filedata', array(), 'array');

Nun ging ich auf die mühseelige Suche, warum get false liefert und habe dazu ersteinmal einen Vergleich vorgenommen, wie $files gefüllt sein muss, wenn ein Upload z.B. bei einer GZ-Datei erfolgreich ausgeführt wird. Dabei zeigte $files folgende Rückgabe per print_r():

Array
(
   [0] => Array
     (
       [name] => meinegz.gz
       [type] => application/gzip
       [tmp_name] => F:\xampp\tmp\phpE246.tmp
       [error] => 0
       [size] => 903138
     )
)

Bei meinen ZIP-Dateien dagegen war das $files-Array leer.
Nun suchte ich nach dem Script welches die get()-Methode ausführte um den hier verwendeten Argumentesatz zu verstehen. Hier die Erkenntnisse dazu:

Die Methode $this->input->files->get() wird in libraries/vendor/joomla/input/src/Input.php geholt. In get() wiederrum wird die o.g. Methode $this-filter->clean() ausgeführt.
S. auch libraries/vendor/joomla/input/src/Files.php die Methode get(), hier wird das zurückgelieferte o.g. Array aufgebaut, welches an die ArrayVariable $files im o.g. Script file.php übergeben wird. Hierin wird die Methode decodeData() genutzt.

In der o.g. Methode clean() sehen wir, dass dort diverse Filtertypen definiert sind, u.a. auch unser "array"-Typ. Neben diesem Typ gibt es den Typ "raw" der mich auf eine heiße Spur lockte.

Es zeigte sich, dass wenn man den Filter raw statt array verwendet, keine Filter/Prüfung stattfindet. Folglich habe ich meine Script so modifiziert, dass raw wie folgt in o.g. Zeile eingebaut wurde um $files erfolgreich zu füllen:

$files = $this->input->files->get('Filedata', array(), 'raw');

Das brachte erstmals einen erfolgreichen ZIP-Upload zustande. Nun bin ich mir nicht bewußt, ob hier durch Joomla trotzdem noch eine Sicherheitsprüfung für die Uploaddatei erfolgt. Aber für meinen eingegrenzten Einsatzfall war das nicht entscheidend.

Anmerkung 1: Gegenüber meinen Sicherheitsbedenken ist erwähnen, dass im Joomla-Installer-Script (adminstrator/components/com_installer/modesl/install.php) genau diese Filtertyp raw ebenfalls eingesetzt wird mit dem Inline-Kommentar
// Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See JInputFiles::get.
Und im gleichen Script wird übrigens auch der o.g. Parameter allow_unsafe mit true übergeben:
JFile::upload($tmp_src, $tmp_dest, false, true);
Daraus möchte ich schließen, dass es durchaus legitim ist, unter bestimmten Notwendigkeiten diese Restriktionen aufzuheben.

Anmerkung 2: Nun zeigte sich ein etwas eigenartiges Phänomen. Ich habe mir wieder den Inhalt von $files angeschaut wie er nun für den erfolgreichen ZIP-Upload aussah. Hier das Ergebnis:

Array
(
    [0] => Array
        (
            [name] => meinezip.zip
            [type] => application/octet-stream
            [tmp_name] => F:\xampp\tmp\php5D9B.tmp
            [error] => 0
            [size] => 2622587
        )

)

Im Vergleich zu oben aufgeführtem GZ-Upload steht hier nun unerwarteterweise für den Mimetype application/octet-stream. Dieser Type ist eigentlich typische für Video-Dateien aber auch andere nicht weiter spezifizierte Mimetypen wie bin, exe, com etc. also Binärdateien im allgemeinen. Das deutet wohl daraufhin, dass durch die Verwendung von raw keine inhaltliche Untersuchung und Feststellung des Mimetypes erfolgt und folglich mit octet-stream gefüllt wird.

relevante core-Scripte:
libraries/vendor/joomla/input/src/Files.php
libraries/vendor/joomla/input/src/Input.php
libraries/vendor/joomla/filter/src/InputFilter.php