Von der Hochladen bis zur URL: sicheres Speichern und Bereitstellen von Benutzerdateien in Express
Erfahren Sie, wo Express-Anwendungen hochgeladene Dateien speichern sollten, wie express.static einen Ordner auf URLs abbildet und welche Schutzmaßnahmen verhindern, dass Benutzer hochgeladene Dateien zu Sicherheitslücken werden.
Ein Benutzer wählt ein Profilbild aus, klickt auf „Hochladen“ und bereits eine Sekunde später erscheint es auf seiner Seite. Hinter dieser einfachen Interaktion stehen drei tatsächliche Designentscheidungen: Wo der Datei physisch gespeichert wird, wie der Server sie wieder findet und wie ein Browser sie anzeigt. Dieser Leitfaden behandelt alle drei Aspekte für eine Express-Anwendung – von der Auswahl eines Speicherorts über die Bereitstellung der Dateien über eine URL bis hin zu den Vorsichtsmaßnahmen, die verhindern, dass Hochlader zu einer Angriffsfläche werden.
Speicherung der Hochgeladenen auf der Festplatte
Bevor eine Datei erneut bereitgestellt werden kann, muss sie irgendwo gespeichert werden. In der einfachsten Konfiguration handelt es sich dabei um einen Ordner im eigenen Dateisystem des Servers, der in der Regel uploads genannt wird und sich am Projektroot neben dem Quellcode befindet – nicht darin:
project-root/
uploads/
avatars/
user-42.png
user-91.png
documents/
invoice-108.pdf
src/
app.js
Die Gruppierung von Dateien nach Zweck, wie z. B. avatars/ und documents/, bringt schnell Vorteile. Sie hält die Verzeichnisübersichten bei wachsendem Umfang übersichtlich, erleichtert die Anwendung von Aufräum- und Backup-Strategien pro Kategorie und erzeugt, wie Sie gleich sehen werden, URLs, die leicht vorhersehbar sind. Ein einziger flacher Ordner mit allen Arten von Dateien wird bereits lange bevor es zu Leistungsproblemen kommt, schwer verständlich.
In den meisten Anwendungen speichert die Datenbank nur einen Verweis auf die Datei, wie zum Beispiel den relativen Pfad avatars/user-42.png, nicht die Datei selbst und auch keinen vollständigen URL. Durch das Speichern des relativen Pfads kann man später den Domainnamen ändern oder auf eine andere Speicherschnittstelle wechseln, ohne jede Zeile neu schreiben zu müssen.
Lokaler Festplattenspeicher oder externer Objektspeicher
Dateien auf dem Anwendungsserver speichern
Das Direkt-Schreiben von Dateien auf die Festplatte des Servers, wie im obigen Beispiel dargestellt, erfordert keine zusätzliche Infrastruktur. Dies eignet sich gut für lokale Entwicklung, Prototypen sowie interne Tools und Anwendungen, deren Upload-Menge gering und vorhersehbar ist.
Übertragung von Dateien in einen Speicherdienst
In Produktivsystemen werden Uploads häufig an einen speziellen Objektspeicher weitergeleitet, beispielsweise Amazon S3, Google Cloud Storage oder einen ähnlichen Dienst, wodurch die Festplatte des Anwendungs Servers nicht betroffen wird.
Warum die Wahl wichtig ist
- Die Festplatten können klein und von kurzer Dauer sein. Viele Hosting-Plattformen bauen oder ersetzen Instanzen bei jeder Bereitstellung um, sodass eine Datei, die nur auf der lokalen Festplatte existiert, beim nächsten Deployment verschwinden kann.
Nichts davon macht den lokalen Speicher falsch. Für Lernprojekte und wirklich kleine Anwendungen ist er ein sinnvoller Erstschritt, und die unten genannten Ansätze (geordnete Pfade, vorhersehbare URLs, sorgfältige Validierung) bleiben unverändert, wenn Sie später auf externe Speicher wechseln. Wenn der Speichercod in einem kleinen Modul mit Funktionen wie save und getUrl untergebracht ist, wird dieser Umstieg deutlich unkomplizierter.
Verwenden von express.static zum Bereitstellen des Verzeichnisses
Sobald die Datei gespeichert ist, benötigt der Browser dennoch einen Weg, um sie abzurufen. Express bietet ein Middleware für statische Dateien genau zu diesem Zweck:
const express = require("express");
const app = express();
app.use("/uploads", express.static("uploads"));
Diese Zeile montiert das uploads-Verzeichnis unter dem URL-Prefix /uploads. Für jede Anfrage, deren Pfad mit diesem Prefix beginnt, sucht Express nach einer Datei an der entsprechenden Stelle im Verzeichnis und streamt sie bei Vorhandensein zusammen mit dem passenden Inhaltstyp sowie Caching-Headern zurück. Man muss keinen Handler pro Datei schreiben.
Das ist ein anderes Denkmuster als bei einer gewöhnlichen API-Route. Anstelle von Logik, die Daten lädt und eine Antwort erstellt, werden ganze Verzeichnisse veröffentlicht, wobei die Zuordnung ausschließlich über den Pfad erfolgt. Daraus ergeben sich zwei praktische Hinweise:
- Der Ordnerparameter wird relativ zum Verzeichnis berechnet, aus dem der Node-Prozess gestartet wurde – nicht zum Dateiinhalt. Die Verwendung eines absoluten Pfads, wie z. B. mit
path.join(__dirname, „..“, „uploads“), vermeidet Überraschungen, wenn die Anwendung aus einem anderen Arbeitsverzeichnis gestartet wird. - Alles, was in diesem Ordner platziert wird, ist öffentlich zugänglich. Mounten Sie nur das Verzeichnis, das tatsächlich freigegeben werden soll.
Wie eine gespeicherte Datei zu einer URL wird
Sobald die statische Bereitstellung eingerichtet ist, spiegeln sich der Pfad auf dem Datenträger und der Pfad in der URL gegenseitig wider:
File on disk: uploads/avatars/user-42.png
Accessible at: https://yourapp.com/uploads/avatars/user-42.png
Durch diese eins-zu-eins-Übertragung kann die Frontend-Seite direkt auf die Datei verweisen, beispielsweise in einer Bildtag, ohne dass ein spezieller API-Endpunkt zum Herunterladen der Bytes benötigt wird:
<img src="https://yourapp.com/uploads/avatars/user-42.png" alt="User avatar" />
Hier kommt die Ordnerstruktur aus dem ersten Abschnitt zum Tragen. Die von Ihnen auf der Festplatte gewählte Struktur entspricht genau der, die die Clients in den URLs sehen – daher sorgt eine ordentliche, konsistente Struktur für übersichtliche und einheitliche Links. Zudem ermöglicht dies es, die URL anhand des in der Datenbank gespeicherten relativen Pfads zu erstellen, wodurch Domain und Präfix in einem einzigen Konfigurationswert zusammengefasst werden.
Sicherheitsmaßnahmen für Benutzeruploads
Uploads sind ein beliebtes Ziel für Missbrauch, weshalb „der Upload funktioniert“ lediglich der Ausgangspunkt ist. Jede der folgenden Maßnahmen schließt ein spezifisches Sicherheitsloch.
Überprüfen, was die Datei tatsächlich ist
Eine Dateinamenerweiterung ist lediglich vom Client gewählter Text und beweist nichts. Prüfen Sie den tatsächlichen Inhaltstyp, idealerweise indem Sie die ersten Bytes der Datei lesen anstatt dem deklarierten MIME-Typ zu vertrauen, und akzeptieren Sie nur das, was die Funktion benötigt. Ein Avatar-Endpunkt sollte beispielsweise ausschließlich Bilder entgegennehmen und nichts anderes. Formate wie SVG und HTML erfordern besondere Vorsicht, da Browser darin eingebettete Skripte ausführen können.
Größe begrenzen
Legen Sie eine maximale Größe für jeden Upload fest. Ohne Limit kann eine einzige überdimensionierte Anfrage, ob versehentlich oder absichtlich, den Speicher füllen oder die Bandbreite überlasten. Setzen Sie das Limit bereits beim Parsen der Anfrage durch, damit der Server frühzeitig mit dem Lesen aufhört, anstatt zunächst den gesamten Inhalt zu akzeptieren.
Eigene Dateinamen erzeugen
Schreiben Sie niemals eine Datei unter dem Namen, den der Kunde bereitgestellt hat. Dieser Name kann ungewöhnliche Zeichen oder Pfadüberschreitungssequenzen wie ../../config.js enthalten, die dazu dienen, dem Upload-Verzeichnis zu entkommen. Erstellen Sie auf dem Server einen neuen Namen, beispielsweise eine zufällige Identifikation zusammen mit einer überprüften Erweiterung. Als Nebeneffekt verhindert dies außerdem, dass zwei Benutzer, die beide photo.jpg hochladen, sich gegenseitig überschreiben.
Halten Sie das Upload-Verzeichnis isoliert
Speichern Sie die hochgeladenen Dateien in einem Verzeichnis, das klar von Quellcode und Konfiguration getrennt ist, und stellen Sie sicher, dass das statische Middleware-Modul nur dieses Verzeichnis freigibt. Ein versehentlicher Mount des Projektverzeichnisses würde Umgebungsdateien und Quellcode allen zugänglich machen, die sich den Pfad ausrechnen können.
Überlegen Sie, welcher Ursprung die Dateien bereitstellt
Die Bereitstellung von vom Benutzer bereitgestelltem Inhalt aus demselben Domainnamen wie die Hauptanwendung kann Probleme wie Cross-Site-Scripting verursachen, da ein bösartiger Datei mit der Origin Ihres Sites ausgeführt wird. Aus diesem Grund liefern viele Produktivsysteme hochgeladene Dateien aus externem Speicher oder einer separaten Subdomain. Das Senden des Headers X-Content-Type-Options: nosniff sowie, für Downloads, die nicht inline dargestellt werden sollen, des Headers Content-Disposition: attachment, verringert das Risiko weiter.
Häufige Fragen
Muss jedes Projekt S3 oder einen ähnlichen Dienst verwenden?
Nicht unbedingt. Für kleine oder lernende Projekte ist der lokale Speicher einfacher und völlig ausreichend. Externer Speicher wird wichtig, sobald Sie auf einer Plattform mit einem vorübergehenden Dateisystem arbeiten oder mehrere Instanzen betreiben, die auf dieselben Dateien zugreifen müssen.
Braucht express.static ein zusätzliches Paket?
Nicht, es ist in Express integriert. Die Verarbeitung des eingehenden Uploads aus einem multipart Formular erfordert in der Regel eine Bibliothek wie Multer, doch das Bereitstellen bereits gespeicherter Dateien nicht.
Ist es zulässig, den ursprünglichen Dateinamen beizubehalten?
Es ist sicherer, dies nicht zu tun. Ein vom Server generierter Name vermeidet seltsame Zeichen, Kollisionen bei identischen Namen sowie Manipulationstricks, die in einem künstlich erstellten Dateinamen verborgen sein können. Falls Benutzer den ursprünglichen Namen sehen müssen, sollte dieser stattdessen als Metadaten in der Datenbank gespeichert werden.
Kann der Zugriff auf hochgeladene Dateien eingeschränkt werden?
Ja, aber nicht allein mit express.static, da diese Funktion eine Datei jedem zur Verfügung stellt, der ihre URL kennt. Für private Dateien ist ein spezieller Pfad erforderlich, der vor dem Streamen der Datei Authentifizierung und Autorisierung überprüft – oder bei externer Speicherung kurzlebige Links, die nur für autorisierte Benutzer erstellt werden.
Zusammenfassung
Eine zuverlässige Hochladefunktion beantwortet nacheinander vier Fragen. Wo befindet sich die Datei: auf der Festplatte des Servers, weil das einfach ist, oder in einem Objektspeicher, weil sie Deploys übersteht und skaliert? Wie findet die Anwendung sie später: über eine geordnete Ordnerstruktur und einen in der Datenbank gespeicherten relativen Pfad? Wie lädt ein Browser sie herunter: über express.static, das auf einen klaren URL-Präfix verknüpft ist, oder über einen geschützten Pfad für private Dateien? Und was schützt sie: überprüfte Datentypen, Größenbeschränkungen, vom Server generierte Namen sowie eine eng abgegrenzte, idealerweise separate, Bereitstellungsstelle. Wenn diese Aspekte geklärt sind, ist der Rest der Funktionalität größtenteils nur technische Umsetzung.