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.
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 Standardund 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.
