Fast alles, was Ihre Anwendung von außen annimmt, ist Text: Formularfelder, Suchbegriffe, JSON. Der Datei-Upload ist die Ausnahme. Dort schreibt ein Fremder beliebige Daten auf Ihren Server, ob als Profilbild, Rechnung, Bewerbung oder CSV-Import. Gebaut wurde diese Funktion meist einmal, vor Jahren, und seither hat sie niemand mehr angesehen, weil sie funktioniert.
Dieser Beitrag bezieht sich auf Laravel, weil es so verbreitet ist. Die Fragen gelten für jede PHP-Anwendung.
Was ein unsicherer Upload kostet
Drei Schäden sind typisch, und keiner davon fällt im Alltag auf.
Der schwerste: Jemand lädt ein PHP-Skript hoch, das sich als Bild ausgibt. Landet es in einem Ordner, der aus dem Internet erreichbar ist, genügt der Aufruf der Adresse, und das Skript läuft mit den Rechten Ihrer Anwendung. Damit sind Datenbank-Zugangsdaten und Kundendaten lesbar.
Der zweite betrifft Ihre Nutzer. Eine SVG-Grafik ist eine Textdatei und kann JavaScript enthalten. Lädt ein Nutzer so eine Datei hoch und ein anderer öffnet sie im Browser, läuft das Skript in dessen Sitzung. Ist der andere ein Administrator, gehört dem Angreifer dessen Zugang.
Der dritte braucht gar keinen Angriff. Liegen alle Uploads in einem öffentlichen Ordner, kann jeder, der eine Adresse kennt oder errät, die Rechnungen und Ausweiskopien Ihrer Kunden abrufen, ohne angemeldet zu sein. Das ist ein meldepflichtiger Datenschutzvorfall, sobald es jemand bemerkt.
Fünf Fragen an Ihren Entwickler
Wo liegen die Dateien, und sind sie direkt per Adresse erreichbar?
Das ist die wichtigste der fünf. Jede Prüfung lässt sich mit genug Aufwand umgehen. Eine Datei, die außerhalb des öffentlichen Bereichs liegt, kann aber weder ausgeführt noch erraten werden. In Laravel heißt die richtige Antwort: privater Disk, Auslieferung über einen Controller, der bei jedem Abruf prüft, ob dieser Nutzer diese Datei sehen darf. Die bedenkliche Antwort lautet public/uploads oder "liegt unter /storage, den Link kennt ja keiner".
Unter welchem Namen wird gespeichert?
Der Dateiname kommt vom Absender und ist damit frei wählbar: ../../config.php, .htaccess, rechnung.php.jpg. Auf dem Server sollte davon nichts ankommen. Die Anwendung vergibt einen eigenen, zufälligen Namen und merkt sich den Originalnamen nur in der Datenbank, zur Anzeige. Laravels store() macht das von selbst. Wo im Code storeAs() zusammen mit getClientOriginalName() steht, hat jemand diesen Schutz abgeschaltet.
Woran erkennt die Anwendung, was für eine Datei das ist?
Hier liegt der häufigste Irrtum. Dateiendung und der vom Browser gemeldete Typ sind Behauptungen des Absenders. Benennen Sie eine PHP-Datei in bild.jpg um, und beide sagen "Bild". Verlässlich ist nur, was der Server aus dem Inhalt der Datei liest.
// glaubt dem Absender
$file->getClientOriginalExtension();
$file->getClientMimeType();
// liest den Inhalt, begrenzt die Größe, speichert privat unter Zufallsnamen
$request->validate([
'beleg' => ['required', File::types(['pdf', 'jpg', 'png'])->max(5 * 1024)],
]);
$path = $request->file('beleg')->store('belege', 'local');
Laravel bringt die richtige Prüfung also mit. Unsicher wird es dort, wo jemand sie von Hand nachgebaut hat, und in älterem PHP-Code ohne Framework, der sich auf $_FILES['datei']['type'] verlässt. Auch dieser Wert stammt vom Browser.
Welche Dateitypen sind erlaubt, und wer hat das entschieden?
Jeder erlaubte Typ ist eine Angriffsfläche, die Sie in Kauf nehmen. Die Liste sollte so kurz sein wie der fachliche Bedarf, und für jeden Eintrag sollte jemand den Grund nennen können. SVG, HTML und ZIP-Archive brauchen einen besonders guten. Gibt es für SVG keinen, ist das Streichen eine Zeile Arbeit. Zur selben Frage gehört die Obergrenze für die Dateigröße, in der Anwendung und in der PHP-Konfiguration, sonst füllt ein einzelner Absender die Festplatte.
Wie kommen die Dateien wieder heraus?
Eine hochgeladene Datei sollte der Browser eines anderen Nutzers herunterladen und nicht ausführen. Dafür sorgt die Auslieferung als Anhang, mit dem Typ, den der Server ermittelt hat. Bilder, die im Browser angezeigt werden müssen, kann die Anwendung beim Hochladen neu kodieren. Dabei geht verloren, was jemand zusätzlich in der Datei versteckt hat.
Wer hat das zuletzt geprüft?
Ein Upgrade des Frameworks behebt nichts davon. Wo Ihre Anwendung Dateien ablegt und wie sie sie benennt, steht in Ihrem eigenen Code, und den fasst ein Laravel-Update nicht an. Der Hoster ist dafür ebenso wenig zuständig.
In den meisten Anwendungen gibt es außerdem mehr als einen Upload. Das Profilbild wurde sauber gebaut, der CSV-Import im Adminbereich drei Jahre später unter Zeitdruck, und der Anhang im Support-Formular stammt von einem Entwickler, der nicht mehr da ist. Geprüft ist das Thema erst, wenn jemand alle Wege aufgelistet und für jeden die fünf Fragen beantwortet hat. In einem Code-Review gehört das zu den ersten Stellen, die ich lese.
Brauchen Sie dafür eine Migration?
Meistens nicht. Einen Upload abzusichern ist eine abgegrenzte Reparatur, solange die Anwendung auf einer unterstützten Laravel-Version läuft und die Upload-Logik an einer oder wenigen Stellen sitzt.
Größer wird es in drei Fällen:
- Die Upload-Logik ist über viele Controller kopiert und jede Kopie ist etwas anders. Dann lohnt es, sie zuerst an eine Stelle zu ziehen, wie in Legacy-Laravel refaktorisieren beschrieben.
- Die Laravel- oder PHP-Version bekommt keine Sicherheitsupdates mehr. Der Upload ist dann nur eine von mehreren offenen Stellen, und die Reihenfolge sollte ein Befund festlegen.
- Die Anwendung ist älteres PHP ohne Framework. Dort fehlt die mitgelieferte Prüfung, und sie muss gebaut werden.
Zwei Dinge gehen bei der Reparatur gern unter. Die bereits vorhandenen Dateien müssen mit umziehen: Wer nur neue Uploads absichert, lässt zehn Jahre alte Dokumente öffentlich liegen, darunter möglicherweise ein Skript, das längst hochgeladen wurde. Und strengere Prüfungen brauchen verständliche Fehlermeldungen. Sagt die Anwendung nur "ungültige Datei", schicken Ihre Kunden die Unterlagen ab morgen wieder per E-Mail.