Zum Inhalt springen

Der Datei-Upload, der zur Shell wird

Den Mime-Typ zu prüfen sagt nicht, was eine Datei ist, und Uploads unterhalb des Document Root zu speichern heißt, dass der Server bereitwillig ausführt, was falsch geprüft wurde.

4 Min. Lesezeit

Ein Upload-Formular ist eine Route, die einer anonymen Person erlaubt, eine Datei auf Ihren Server zu legen. Die meisten Arten, wie das schiefgeht, stammen aus zwei Entscheidungen der ersten Baustunde.

Was Validierung tatsächlich aussagt

$request->validate([
    'avatar' => 'required|file|mimes:jpg,png|max:2048',
]);

Das ist vernünftig, und man vertraut ihm zu sehr. mimes betrachtet den Inhalt der Datei statt ihren Namen, was eine echte Verbesserung gegenüber der Endungsprüfung ist - aber "beginnt mit einem gültigen PNG-Header" und "ist nur ein PNG" sind nicht dieselbe Aussage. Eine Datei kann einen legitimen Bild-Header tragen und danach PHP-Quelltext, und die Regel trotzdem erfüllen.

Was das harmlos macht, ist nicht bessere Validierung. Es ist, dass nichts die Datei ausführen kann.

Zwei Regeln, die nichts kosten und sich ohnehin lohnen:

'avatar' => 'required|image|dimensions:max_width=4000,max_height=4000|max:2048',

image ist strenger als mimes für Bild-Uploads, und dimensions weist die Dekomprimierungsbombe ab - eine kleine Datei, die sich beim Öffnen durch Ihren Thumbnailer zu etwas aufbläht, das den Speicher erschöpft.

Vertrauen Sie getClientOriginalName() oder getClientMimeType() für nichts. Beides liefert der Client. Der Originalname darf als Anzeigebezeichnung gespeichert werden; daraus einen Pfad zu bauen, darf er nicht.

Die eigentlich entscheidende Wahl: wo die Datei landet

// falsch für Nutzer-Uploads
$request->file('avatar')->store('avatars', 'public');

Die public-Disk ist ein Symlink in Ihr Web-Verzeichnis. Alles dort liefert der Webserver direkt aus, und wenn er so konfiguriert ist, dass er PHP in diesem Verzeichnis ausführt - was bei vielen der Fall ist, per Standard oder aus Versehen -, ist eine Datei, die durch die Validierung kam, jetzt eine URL, die Code ausführt.

Speichern Sie Uploads auf einer privaten Disk:

$path = $request->file('avatar')->store('avatars');   // privat per Standard

und liefern Sie sie über eine Route aus, die prüft, wer fragt:

public function show(Attachment $attachment)
{
    $this->authorize('view', $attachment);
 
    return Storage::download($attachment->path, $attachment->original_name);
}

Diese Route kostet ein wenig Leistung und kauft zwei Dinge auf einmal: nichts ist ausführbar, und der Zugriff ist autorisiert. Objektspeicher mit signierten, ablaufenden URLs ist dieselbe Anordnung mit verlagerter Bandbreite.

Namen, Pfade und das Verzeichnis darüber

Lassen Sie das Framework den Speichernamen erzeugen. store() erzeugt bereits einen zufälligen Namen, was eine ganze Problemklasse entfernt: Path Traversal über einen präparierten Dateinamen, Dateien, die sich gegenseitig überschreiben, und Namen, die einer Shell etwas Unschönes bedeuten.

Behalten Sie den Originalnamen in einer Datenbankspalte zur Anzeige. Das sind zwei verschiedene Dinge, und sie zu vermengen ist, woher Traversal-Fehler kommen.

Das Ausliefern ist, wo das restliche Risiko liegt

SVG ist kein Bild. Es ist ein XML-Dokument, das Skript enthalten kann, und ein Browser führt dieses Skript in dem Origin aus, aus dem es kam. Wenn Sie SVG annehmen, bereinigen Sie es mit einer dafür gebauten Bibliothek, oder liefern Sie Nutzerdateien von einer eigenen Domain aus, damit überlebendes Skript dort läuft, wo es Ihr Session-Cookie nicht erreicht.

Setzen Sie den Content-Type selbst. Liefern Sie einen gespeicherten Typ aus, den Sie entschieden haben, nicht einen beim Download abgeleiteten.

Erzwingen Sie einen Download, wo möglich. Content-Disposition: attachment heißt, der Browser speichert statt zu rendern, was das meiste neutralisiert, was eine feindliche Datei auf einer Seite anrichten könnte.

Entfernen Sie Metadaten aus Bildern. Fotos tragen EXIF, und EXIF trägt GPS-Koordinaten. Ein unbearbeitet veröffentlichtes Nutzerfoto veröffentlicht womöglich, wo es aufgenommen wurde.

Die Grenzen, die niemand setzt, bis etwas kaputtgeht

Eine Größenbeschränkung in der Validierung ist keine Beschränkung dessen, was Ihren Server erreicht - upload_max_filesize und post_max_size in PHP entscheiden das, und der Webserver hat seine eigene, bevor PHP etwas sieht. Setzen Sie alle drei bewusst und sorgen Sie dafür, dass ein Nutzer beim Überschreiten eine Meldung bekommt statt einer leeren Seite.

Begrenzen Sie die Rate der Upload-Route. Ein Endpunkt, der Dateien ohne Grenze annimmt, ist eine Möglichkeit, Ihre Festplatte von außen zu füllen, und eine volle Festplatte reißt alles andere mit.

Prüfung, wenn Sie ein Verteilkanal sind

Laden Nutzer Dateien hoch, die andere Nutzer herunterladen, ist das Risiko nicht mehr nur Ihres. Prüfen Sie asynchron - ein eingereihter Job nach dem Upload, mit der Datei als nicht verfügbar markiert, bis sie freigegeben ist -, damit eine langsame Prüfung nicht in der Anfrage sitzt.

Die Kurzfassung

Mit image und dimensions validieren, privat mit erzeugtem Namen speichern, über eine autorisierte Route mit selbst gewähltem Content-Type ausliefern, und Uploads nie dort landen lassen, wo der Webserver bereit ist, sie auszuführen.

Ist der Speicherort richtig, wird die Validierung zur Bequemlichkeit statt zu dem, was zwischen Ihnen und einer Shell steht.

Die Route, die die Datei ausliefert, braucht die andere Hälfte davon: eine Autorisierungsprüfung, keinen unerratbaren Dateinamen. Beides steht auf der Liste, die ein Audit abarbeitet. Keines von beiden fällt in einem Code-Review auf, denn mit dem Code, der da ist, stimmt alles.

Verwandte Fragen

Reicht die mimes-Validierungsregel?
Sie ist besser als eine Endungsprüfung, weil sie die Datei statt den Namen betrachtet. Allein genügt sie trotzdem nicht - eine Datei kann einen gültigen Bild-Header tragen und danach PHP. Die Regel, die Ausführung tatsächlich verhindert, ist, Uploads dort abzulegen, wo nichts sie ausführt.
Wo sollten Uploads gespeichert werden?
Außerhalb des Document Root oder in einem Objektspeicher, ausgeliefert über eine Route, die Autorisierung prüft. Die public-Disk ist ein Symlink in das Web-Verzeichnis, was für Assets richtig ist, die öffentlich sein sollen, und falsch für alles, was ein Nutzer hochgeladen hat.
Was ist mit SVG-Dateien?
SVG ist ein Dokument, kein Bild - es kann Skript enthalten, und ein Browser führt es aus, wenn die Datei von Ihrer Domain kommt. Entweder SVG-Uploads ablehnen, oder mit einer dafür gebauten Bibliothek bereinigen, oder von einer eigenen Domain ausliefern, damit Skript außerhalb Ihres Origins läuft.
Brauchen wir Virenprüfung?
Wo Nutzer Dateien miteinander teilen, ja - Sie sind dann ein Verteilkanal, und das Risiko geht auf die Herunterladenden über. Prüfen Sie asynchron nach dem Upload und markieren Sie Dateien als nicht verfügbar, bis sie freigegeben sind.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite