Sichere Datei-Uploads in Web-Apps benötigen strikte Berechtigungen, Größenlimits, signierte URLs und einfache Malware-Scanning-Patterns, um Vorfälle zu vermeiden.

Datei-Uploads sehen harmlos aus: ein Profilbild, ein PDF, eine Tabelle. Trotzdem sind sie oft der erste Sicherheitsvorfall, weil sie Fremden erlauben, deinem System eine Wundertüte zu schicken. Wenn du sie annimmst, speicherst und anderen wieder zeigst, schaffst du einen neuen Angriffsvektor auf deine App.
Das Risiko ist nicht nur „jemand lädt ein Virus hoch“. Eine bösartige Datei kann private Dokumente leaken, deine Speicherkosten explodieren lassen oder Nutzer dazu bringen, Zugangsdaten preiszugeben. Eine Datei namens „invoice.pdf“ ist vielleicht gar kein PDF. Selbst echte PDFs und Bilder können Probleme verursachen, wenn deine App Metadaten vertraut, Inhalte automatisch vorschaut oder sie mit falschen Regeln ausliefert.
Typische Ausfälle sehen so aus:
Ein Detail verursacht viele Vorfälle: Speichern ist nicht gleich Ausliefern. Storage ist, wo du die Bytes ablegst. Serving ist, wie diese Bytes an Browser und Apps geliefert werden. Probleme entstehen, wenn die App Nutzer-Uploads mit dem gleichen Vertrauensniveau und den gleichen Regeln ausliefert wie die Hauptseite, sodass der Browser das Upload als „vertrauenswürdig“ einstuft.
„Sicher genug“ für eine kleine oder wachsende App heißt meist, dass du vier Fragen ohne Ausflüchte beantworten kannst: Wer darf hochladen, was akzeptierst du, wie groß und wie oft, und wer darf die Datei später lesen. Selbst wenn du schnell baust (generierter Code oder Chat-gesteuerte Plattform), sind diese Leitplanken wichtig.
Behandle jeden Upload wie untrusted input. Der praktische Weg, Uploads sicher zu halten, ist, dir vorzustellen, wer sie missbrauchen könnte und was für Erfolge dieser Missbrauch bringen würde.
Die meisten Angreifer sind entweder Bots, die nach schwachen Upload-Formularen scannen, oder echte Nutzer, die Grenzen ausreizen, um kostenlosen Speicher zu bekommen, Daten zu scrapen oder zu trollen. Manchmal ist es ein Wettbewerber, der auf Lecks oder Ausfälle sondiert.
Was wollen sie erreichen? Meistens eines der folgenden Ziele:
Dann mappe die Schwachstellen: Der Upload-Endpunkt ist die Eingangstür (zu große Dateien, seltsame Formate, hohe Request-Raten). Der Storage ist der Hinterraum (öffentliche Buckets, falsche Berechtigungen, geteilte Ordner). Download-URLs sind der Ausgang (vorhersehbar, langlebig oder nicht an einen Nutzer gebunden).
Beispiel: eine „Lebenslauf-Upload“-Funktion. Ein Bot lädt tausende große PDFs hoch, um Kosten zu erzeugen, während ein missbräuchlicher Nutzer eine HTML-Datei hochlädt und sie als „Dokument“ teilt, um andere zu täuschen.
Bevor du Controls hinzufügst, entscheide, was für deine App am wichtigsten ist: Privatsphäre (wer darf lesen), Verfügbarkeit (kannst du weiter ausliefern), Kosten (Speicher und Bandbreite) und Compliance (wo Daten liegen und wie lange du sie aufbewahrst). Diese Prioritätenliste sorgt für konsistente Entscheidungen.
Die meisten Upload-Vorfälle sind keine großen Hacks. Es sind einfache „Ich kann die Datei eines anderen sehen“-Bugs. Betrachte Berechtigungen als Teil des Uploads, nicht als etwas, das du später draufsetzt.
Fang mit einer Regel an: Default deny. Geh davon aus, dass jedes hochgeladene Objekt privat ist, bis du explizit Zugriff erlaubst. „Standardmäßig privat“ ist eine starke Grundlage für Rechnungen, medizinische Dateien, Kontodokumente und alles, was an einen Nutzer gebunden ist. Mache Dateien nur dann öffentlich, wenn der Nutzer das klar erwartet (z. B. ein öffentliches Avatar), und erwäge selbst dann zeitlich begrenzten Zugriff.
Halte Rollen einfach und getrennt. Eine übliche Aufteilung ist:
Verlasse dich nicht auf Ordner-Regeln wie „alles in /user-uploads/ ist ok“. Prüfe Eigentum oder Tenant-Zugriff bei jedem Lesevorgang, für jede Datei. Das schützt, wenn jemand das Team wechselt, eine Organisation verlässt oder eine Datei neu zugewiesen wird.
Ein gutes Support-Pattern ist eng gefasst und temporär: gewähre Zugriff auf genau eine Datei, protokolliere es und lasse ihn automatisch ablaufen.
Die meisten Upload-Angriffe beginnen mit einem einfachen Trick: Eine Datei sieht aufgrund ihres Namens oder eines Browser-Headers sicher aus, ist es aber nicht. Behandle alles, was der Client schickt, als untrusted.
Fang mit einer Allowlist (Positivliste) an: Entscheide die genauen Formate, die du akzeptierst (z. B. .jpg, .png, .pdf) und lehne alles andere ab. Vermeide „irgendein Bild“ oder „irgendein Dokument“, es sei denn, du brauchst das wirklich.
Vertraue weder der Dateiendung noch dem Content-Type-Header vom Client. Beides ist leicht zu fälschen. Eine Datei namens invoice.pdf kann ausführbar sein, und Content-Type: image/png kann gelogen sein.
Stärker ist, die ersten Bytes der Datei zu prüfen, oft „magic bytes“ oder Dateisignatur genannt. Viele Formate haben konsistente Header (wie PNG und JPEG). Wenn der Header nicht dem entspricht, was du erlaubst, lehne ab.
Ein praktisches Validierungs-Setup:
Das Umbenennen ist wichtiger, als es klingt. Wenn du nutzereigene Namen direkt speicherst, lädst du Pfad-Tricks, seltsame Zeichen und unbeabsichtigtes Überschreiben ein. Verwende eine generierte ID für den Storage und behalte den Originalnamen nur zur Anzeige.
Für Profilbilder: nur JPEG und PNG akzeptieren, Header verifizieren und Metadaten entfernen, wenn möglich. Für Dokumente: evtl. nur PDF erlauben und alles mit aktiven Inhalten ablehnen. Wenn du später SVG oder HTML erlaubst, behandle sie als potenziell ausführbar und isoliere sie.
Die meisten Upload-Ausfälle sind keine „raffinierten Hackerangriffe“. Es sind riesige Dateien, zu viele Requests oder langsame Verbindungen, die Server blockieren, bis die App nicht mehr reagiert. Behandle jedes Byte als Kostenfaktor.
Wähle ein maximales Size-Limit pro Feature, nicht nur eine globale Zahl. Ein Avatar braucht nicht dasselbe Limit wie ein Steuerdokument oder ein kurzes Video. Setze das kleinstmögliche Limit, das noch normal erscheint, und füge nur bei echtem Bedarf einen separaten „Large Upload“-Weg hinzu.
Durchsetze Limits an mehreren Stellen, weil Clients lügen können: in der App-Logik, am Webserver oder Reverse Proxy, mit Upload-Timeouts und durch frühe Ablehnung, wenn die deklarierte Größe zu groß ist (bevor du den ganzen Body liest).
Konkretes Beispiel: Avatare auf 2 MB begrenzen, PDFs auf 20 MB, und alles Größere braucht einen anderen Flow (z. B. Direct-to-Object-Storage mit signierter URL).
Auch kleine Dateien können zu DoS werden, wenn jemand sie in einer Schleife hochlädt. Füge Rate-Limits für Upload-Endpunkte pro Nutzer und pro IP hinzu. Erwäge strengere Limits für anonymen Traffic im Vergleich zu eingeloggten Nutzern.
Resumable Uploads helfen echten Nutzern mit schlechten Netzen, aber das Session-Token muss eng sein: kurze Gültigkeit, an den Nutzer gebunden und an eine bestimmte Dateigröße und ein Ziel gebunden. Sonst wird der „Resume“-Endpunkt zur kostenlosen Rohrleitung in deinen Speicher.
Wenn du einen Upload blockst, gib klare, nutzerfreundliche Fehler zurück (Datei zu groß, zu viele Anfragen), aber leak keine Interna (Stacktraces, Bucket-Namen, Vendor-Details).
Sichere Uploads betreffen nicht nur, was du akzeptierst. Es geht auch darum, wohin die Datei geht und wie du sie später zurückgibst.
Halte Upload-Bytes aus deiner Hauptdatenbank heraus. Die meisten Apps brauchen nur Metadaten in der DB (Owner-User-ID, Originaldateiname, ermittelter Typ, Größe, Checksumme, Storage-Key, Erstellungszeit). Speichere die Bytes in Objektspeicher oder einem File-Service, der für große BLOBs gebaut ist.
Trenne public und private Dateien auf Storage-Ebene. Verwende verschiedene Buckets oder Container mit unterschiedlichen Regeln. Öffentliche Dateien (z. B. öffentliches Avatar) können ohne Login lesbar sein. Private Dateien (Verträge, Rechnungen, medizinische Dokumente) dürfen niemals öffentlich lesbar sein, auch nicht, wenn jemand die URL errät.
Vermeide es, Nutzerdateien von derselben Domain wie deine App auszuliefern, wenn möglich. Wenn eine riskante Datei durchrutscht (HTML, SVG mit Skripten oder Browser-MIME-Sniffing-Anomalien), kann das Hosten auf deiner Hauptdomain in eine Account-Übernahme münden. Eine eigene Download-Domain (oder Storage-Domain) begrenzt den Schaden.
Beim Download zwinge sichere Header. Setze einen vorhersehbaren Content-Type basierend auf dem, was du erlaubst, nicht auf das, was der Nutzer behauptet. Bei allem, was vom Browser interpretiert werden könnte, schicke es lieber als Download.
Ein paar Defaults, die Überraschungen vermeiden:
Content-Disposition: attachment für Dokumente.Content-Type (oder application/octet-stream).Retention ist auch Sicherheit. Lösche verwaiste Uploads, entferne alte Versionen nach Ersatz und setze Zeitlimits für temporäre Dateien. Weniger gespeicherte Daten bedeuten weniger, was geleakt werden kann.
Signierte URLs (oft pre-signed URLs genannt) sind ein übliches Mittel, um Nutzern Uploads oder Downloads zu erlauben, ohne dein Storage-Bucket öffentlich zu machen und ohne jeden Byte über deine API zu schicken. Die URL trägt temporäre Berechtigung und läuft dann ab.
Zwei gängige Flows:
Direct-to-storage reduziert die API-Last, macht aber Storage-Regeln und URL-Constraints wichtiger.
Behandle eine signierte URL wie einen Einmalschlüssel. Mach sie spezifisch und kurzlebig.
Ein praktisches Muster ist: Lege zuerst einen Upload-Eintrag an (Status: pending), dann gib die signierte URL aus. Nach dem Upload bestätige, dass das Objekt existiert und erwartete Größe und Typ hat, bevor du es als bereit markierst.
Ein sicherer Upload-Flow ist vor allem klare Regeln und klarer Zustand. Behandle jeden Upload als untrusted, bis die Prüfungen abgeschlossen sind.
Schreibe auf, was jedes Feature erlaubt. Ein Profilfoto und ein Steuerdokument sollten nicht dieselben Dateitypen, Größenlimits oder Sichtbarkeiten teilen.
Definiere erlaubte Typen und ein per-Feature Größenlimit (z. B.: Fotos bis 5 MB; PDFs bis 20 MB). Erzwinge dieselben Regeln im Backend.
Erstelle einen „Upload-Record“, bevor Bytes ankommen. Speichere: Eigentümer (User oder Org), Zweck (avatar, invoice, attachment), Originaldateiname, erwartete Max-Größe und einen Status wie pending.
Upload in einen privaten Bereich. Lass den Client nicht den finalen Pfad wählen.
Validere erneut serverseitig: Größe, Magic-Bytes/Typ, Allowlist. Wenn bestanden, setze Status auf uploaded.
Scanne auf Malware und aktualisiere den Status zu clean oder quarantined. Wenn das Scanning asynchron ist, bleibe beim Sperrstatus, bis es abgeschlossen ist.
Erlaube Download, Vorschau oder Verarbeitung erst, wenn der Status clean ist.
Kleines Beispiel: Für ein Profilbild erstellst du einen Datensatz, der an den Nutzer und den Zweck avatar gebunden ist, speicherst es privat, bestätigst, dass es wirklich JPEG/PNG ist (nicht nur so benannt), scannst es und generierst dann eine Vorschau-URL.
Malware-Scanning ist ein Sicherheitsnetz, kein Versprechen. Es fängt bekannte schlechte Dateien und offensichtliche Tricks ab, erkennt aber nicht alles. Das Ziel ist einfach: Risiko reduzieren und unbekannte Dateien standardmäßig harmlos machen.
Ein zuverlässiges Muster ist: zuerst quarantänen. Speichere jeden neuen Upload in einem privaten, nicht öffentlichen Ort und markiere ihn als pending. Erst nach Prüfungen verschiebst du ihn in einen „clean“-Ort oder markierst ihn als verfügbar.
Synchrones Scanning funktioniert nur für kleine Dateien und niedrigen Traffic, weil der Nutzer warten muss. Die meisten Apps scannen asynchron: akzeptiere den Upload, gib einen „processing“-Status zurück, scanne im Hintergrund.
Basic Scanning ist üblicherweise eine Antivirus-Engine (oder ein Service) plus einige Guardrails: AV-Scan, Dateityp-Prüfungen (Magic Bytes), Archiv-Limits (Zip-Bombs, verschachtelte Zips, enorme entpackte Größe) und das Blocken von Formaten, die du nicht brauchst.
Wenn der Scanner fehlschlägt, timeoutet oder „unknown“ meldet, behandle die Datei als verdächtig. Quarantäne sie und gib keinen Download-Link frei. Hier verbrennen Teams sich oft: „Scan fehlgeschlagen“ darf nicht zu „trotzdem ausliefern“ werden.
Wenn du eine Datei blockierst, formuliere die Meldung neutral: „Wir konnten diese Datei nicht akzeptieren. Versuche eine andere Datei oder kontaktiere den Support.“ Behaupte nicht, du hättest Malware detektiert, es sei denn, du bist dir sicher.
Betrachte zwei Features: ein Profilfoto (öffentlich angezeigt) und eine PDF-Quittung (privat, für Abrechnung oder Support). Beides sind Upload-Probleme, aber sie sollten nicht dieselben Regeln teilen.
Für das Profilfoto: streng bleiben: nur JPEG/PNG erlauben, Größenbegrenzung (z. B. 2–5 MB), serverseitig neu kodieren, damit du nicht die ursprünglichen Bytes auslieferst. Nur nach Prüfungen öffentlich speichern.
Für die PDF-Quittung: größere Größe erlauben (z. B. bis 20 MB), standardmäßig privat halten und vermeiden, sie inline von deiner Haupt-App-Domain zu rendern.
Ein einfaches Status-Modell hält Nutzer informiert, ohne Interna preiszugeben:
Signierte URLs fügen sich gut ein: nutze eine kurzlebige signierte URL für den Upload (write-only, ein Objekt-Key). Gib eine separate kurzlebige signierte URL für das Lesen aus und nur, wenn der Status clean ist.
Protokolliere, was du für Untersuchungen brauchst, nicht die Datei selbst: User-ID, File-ID, Typ-Vermutung, Größe, Storage-Key, Zeitstempel, Scan-Ergebnis, Request-IDs. Vermeide es, rohen Inhalt oder sensible Daten aus Dokumenten zu loggen.
Die meisten Upload-Bugs entstehen, weil eine kleine „temporäre“ Abkürzung dauerhaft wird. Geh davon aus, dass jede Datei untrusted ist, jede URL geteilt wird und jede „wir fixen das später“-Einstellung vergessen wird.
Wiederkehrende Fallen:
Content-Type ausliefern und dem Browser erlauben, riskante Inhalte zu interpretieren.Monitoring ist etwas, das Teams überspringen, bis die Storage-Rechnung explodiert. Verfolge Upload-Volumen, Durchschnittsgröße, Top-Uploader und Fehlerquoten. Ein kompromittiertes Konto kann über Nacht heimlich tausende große Dateien hochladen.
Beispiel: Ein Team speichert Avatare unter nutzergenerierten Namen wie „avatar.png“ in einem gemeinsamen Ordner. Ein Nutzer überschreibt die Bilder anderer. Die Lösung ist langweilig, aber effektiv: generiere Objekt-Keys serverseitig, halte Uploads standardmäßig privat und liefere ein skaliertes Bild über eine kontrollierte Antwort aus.
Nutze das als finalen Durchgang, bevor du shippst. Behandle jeden Punkt als Release-Blocker, denn die meisten Vorfälle kommen von einer fehlenden Leitplanke.
Content-Type, sichere Dateinamen und attachment für Dokumente.Schreibe deine Regeln in einfacher Sprache auf: erlaubte Typen, Max-Größen, wer auf was zugreifen darf, wie lange signierte URLs leben und was „Scan bestanden“ bedeutet. Das wird zum gemeinsamen Vertrag zwischen Produkt, Engineering und Support.
Füge ein paar Tests hinzu, die häufige Fehler auffangen: übergroße Dateien, umbenannte Executables, unautorisierte Reads, abgelaufene signierte URLs und „Scan pending“-Downloads. Diese Tests sind billig im Vergleich zu einem Vorfall.
Wenn du schnell baust und iterierst, hilft ein Workflow, in dem du Änderungen planen und sicher zurückrollen kannst. Teams, die Koder.ai (koder.ai) verwenden, nutzen oft Planungsmodus und Snapshots/Rollback beim Verschärfen der Upload-Regeln über die Zeit, aber die Kernanforderung bleibt: die Policy wird im Backend durchgesetzt, nicht in der UI.
Beginne mit standardmäßig privat und behandle jeden Upload als untrusted input. Setze serverseitig vier Grundlagen durch:
Wenn du diese Fragen klar beantworten kannst, bist du den meisten Vorfällen schon voraus.
Weil Nutzer eine „Wundertüte“ hochladen können, die dein System speichert und später anderen ausliefert. Das kann zu führen zu:
Es ist selten nur „jemand hat ein Virus hochgeladen“.
Speichern bedeutet, die Bytes irgendwo abzulegen. Ausliefern bedeutet, wie diese Bytes an Browser oder Apps gesendet werden.
Die Gefahr entsteht, wenn deine App Nutzer-Uploads mit dem gleichen Vertrauen und den gleichen Regeln ausliefert wie die Hauptseite. Wenn eine riskante Datei wie eine normale Seite behandelt wird, kann der Browser sie ausführen (oder Nutzer schenken ihr zu viel Vertrauen).
Eine sichere Default-Strategie ist: zunächst privat speichern und dann über kontrollierte Download-Antworten mit sicheren Headern ausliefern.
Verwende default deny und prüfe den Zugriff bei jedem Download oder jeder Vorschau.
Praktische Regeln:
Vertraue weder der Dateiendung noch dem Content-Type des Browsers. Validierung gehört auf den Server:
Ausfälle resultieren häufig aus langweiligem Missbrauch: zu viele Uploads, riesige Dateien oder langsame Verbindungen, die Server blockieren.
Gute Defaults:
Behandle jedes Byte als Kostenfaktor und jede Anfrage als potenziellen Missbrauch.
Ja, aber mit Vorsicht. Signierte URLs erlauben Browsern, direkt in den Objektspeicher zu laden/zu lesen, ohne das Bucket öffentlich zu machen.
Gute Defaults:
Direct-to-storage reduziert API-Load, macht aber gutes Scoping und kurze Laufzeiten notwendig.
Das sicherste Muster ist:
pendingScanning ist nützlich, aber keine Garantie. Nutze es als Sicherheitsnetz, nicht als alleiniges Kontrollwerkzeug.
Praktischer Ansatz:
Wichtig ist die Policy: „nicht gescannt“ darf nie „verfügbar“ bedeuten.
Liefere Dateien so, dass der Browser sie nicht als Webseiten interpretiert.
Gute Defaults:
Content-Disposition: attachment für DokumenteContent-Type (oder )Die meisten echten Bugs sind einfache „Ich sehe die Datei eines anderen“-Fehler.
Wenn die Bytes nicht zu einem erlaubten Format passen, lehne den Upload ab.
clean oder quarantinedclean istSo verhinderst du, dass „Scan fehlgeschlagen“ oder „wird noch verarbeitet“ versehentlich geteilt werden.
application/octet-streamDas reduziert das Risiko, dass eine hochgeladene Datei zu einer Phishing-Seite oder zur Ausführung von Skripten wird.