Datei-Uploads meistern: umfassender Leitfaden für Entwickler
Ein Datei-Upload kann Ihren Server erreichen und trotzdem scheitern, bevor etwas gespeichert wird. Dieser Leitfaden bietet lokale Handler für Bild-Uploads in Node.js, Flask und PHP, eine Rails-Integration mit Authentifizierung sowie einen Postman-Workflow, um Fehler bei Anfragen, Validierung und Speicherung zu unterscheiden.
Einführung: Was ist ein Datei-Upload?
Ein Browser sendet die Bytes der ausgewählten Datei an einen empfangenden Endpunkt. Bei multipart/form-data
ist jede Datei oder jedes Textfeld ein separater Teil mit einem Feldnamen. Der Empfänger parst diese
Teile, wendet seine Annahmerichtlinie an und schreibt akzeptierte Bytes in den Speicher. Ein vom Client
übermittelter Dateiname oder MIME-Typ ist eine Bezeichnung, kein Nachweis für den Inhalt dieser Bytes.
Eine einfache Funktion für Datei-Uploads einrichten
Wählen Sie einen Handler und führen Sie jeweils nur einen auf 127.0.0.1:3000 aus. Dieses HTML-Formular
sendet eine Datei unter dem Feldnamen myfile:
<form action="http://127.0.0.1:3000/upload" method="post" enctype="multipart/form-data">
<label for="myfile">Image</label>
<input id="myfile" type="file" name="myfile" />
<button type="submit">Upload File</button>
</form>
Das Attribut enctype="multipart/form-data" ist erforderlich, da es den Browser anweist,
Dateidaten zusammen mit normalen Formularfeldern zu senden.
Die folgenden Programme für Node, Flask und PHP demonstrieren lokale Upload-Handler. Binden Sie sie an die Loopback-Adresse und speichern Sie ihre Dateien außerhalb aller statischen Verzeichnisse und Dokumentenstammverzeichnisse. Sie implementieren keine Anmeldung: Ergänzen Sie vor der Bereitstellung Authentifizierung, Autorisierung, CSRF-Schutz, Kontingente pro Nutzer und Rate Limits. Das Rails-Fragment integriert sich in eine bestehende Anwendung mit Authentifizierung.
Verwenden Sie Bash sowie separat installiertes Node.js und Corepack; siehe die Yarn-Installationsanleitung. Getestet wurden Node.js 24.21.0 und Yarn 4.18.0. Diese festen Versionsangaben dienen der Reproduktion und sind keine grundsätzlichen Mindestversionen. Arbeiten Sie außerhalb eines bestehenden JavaScript-Projekts, damit dessen Paketmanager-Einstellungen die Installation nicht beeinflussen können. Fügen Sie diesen Block in Ihre Shell ein; seine Subshell lässt bei einem Fehler Ihr Arbeitsverzeichnis und Ihre Shell-Optionen unverändert:
(
for tool in node corepack; do
command -v "$tool" >/dev/null || { printf 'Missing %s\n' "$tool" >&2; exit 1; }
done
[ -z "${YARN_RC_FILENAME:-}" ] || { printf 'Use the default Yarn configuration filename\n' >&2; exit 1; }
node --input-type=module -e '
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
let directory = process.cwd();
for (;;) {
if (["package.json", "yarn.lock", ".yarnrc.yml", ".pnp.cjs"].some(name => existsSync(join(directory, name)))) {
throw new Error("Choose a directory outside an existing JavaScript project");
}
const parent = dirname(directory);
if (parent === directory) break;
directory = parent;
}
' &&
mkdir upload-node && cd upload-node &&
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.18.0"}' > package.json &&
printf 'nodeLinker: node-modules\nenableGlobalCache: false\n' > .yarnrc.yml &&
corepack yarn add express@5.2.1 express-fileupload@1.5.2 file-type@22.1.0 &&
corepack yarn node --input-type=module -e "await import('express'); await import('express-fileupload'); await import('file-type')"
)
Der Block erstellt upload-node, lässt die aufrufende Shell aber in ihrem ursprünglichen Verzeichnis.
Falls die Installation scheitert, behalten Sie dieses Verzeichnis und seine Dateien. Beheben Sie das
Verbindungsproblem oder den gemeldeten Installationsfehler und versuchen Sie es erneut mit der expliziten
Abhängigkeitsliste. Ein bloßes yarn install kann nach einem frühen Fehler bei der Auflösung
mit einem leeren Manifest erfolgreich sein:
(
cd upload-node &&
corepack yarn add express@5.2.1 express-fileupload@1.5.2 file-type@22.1.0 &&
corepack yarn node --input-type=module -e "await import('express'); await import('express-fileupload'); await import('file-type')"
)
Speichern Sie den Code als upload-node/upload.ts. Der Parser hält Dateidaten im Arbeitsspeicher,
mit bis zu zwei Dateipuffern pro Anfrage. Der Handler verlangt eine angegebene Länge des Anfragekörpers,
erlaubt zwei aktive HTTP-Antworten und erzeugt Speicherpfade auf dem Server. Sein HTTP-Zähler gibt die
Reservierung beim Schließen der Antwort frei; er begrenzt keine asynchronen Schreibvorgänge, die nach
einem Verbindungsabbruch des Clients noch laufen.
import { randomUUID } from 'node:crypto'
import { mkdir, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import express from 'express'
import fileUpload from 'express-fileupload'
import { fileTypeFromBuffer } from 'file-type'
const app = express()
const maxBytes = 5 * 1024 * 1024
const root = join(process.cwd(), 'private-uploads')
await mkdir(root, { recursive: true, mode: 0o700 })
let active = 0
const parse = fileUpload({
// Busboy signals its limit at the threshold; the extra byte keeps maxBytes inclusive.
limits: { fileSize: maxBytes + 1, files: 2, fields: 0, parts: 3 },
abortOnLimit: true,
useTempFiles: false,
uploadTimeout: 10_000,
})
app.post('/upload', (req, res, next) => {
const length = req.get('content-length') ?? ''
if (!/^[1-9][0-9]*$/.test(length)) return res.status(411).send('Content length required')
if (Number(length) > 6 * 1024 * 1024) return res.status(413).send('Request too large')
if (active >= 2) return res.status(503).send('Busy')
active += 1
res.once('close', () => { active -= 1 })
next()
}, parse, async (req, res) => {
const files = req.files
const file = files?.myfile
if (!files || Object.keys(files).length !== 1 || !file || Array.isArray(file)) {
return res.status(400).send('Send exactly one myfile')
}
if (file.truncated || file.size < 1 || file.size > maxBytes) {
return res.status(413).send('Invalid file size')
}
let type
try {
type = await fileTypeFromBuffer(file.data.subarray(0, 4100))
} catch {
return res.status(400).send('Unrecognized image')
}
const extensions = new Map([['image/jpeg', 'jpg'], ['image/png', 'png'], ['image/gif', 'gif']])
const extension = extensions.get(type?.mime)
if (!extension) return res.status(400).send('Unrecognized image')
const directory = join(root, randomUUID())
let created = false
try {
await mkdir(directory, { mode: 0o700 })
created = true
await writeFile(join(directory, 'image.' + extension), file.data, { flag: 'wx', mode: 0o600 })
if (res.destroyed) {
await rm(directory, { recursive: true, force: true })
return
}
res.status(201).send('File uploaded successfully')
} catch {
if (created) await rm(directory, { recursive: true, force: true })
throw new Error('Storage failed')
}
})
app.use((_error, _req, res, _next) => {
if (!res.headersSent) res.status(500).send('Upload failed')
})
const server = app.listen(Number(process.env.PORT ?? 3000), '127.0.0.1', (error) => {
if (error) {
console.error('Upload server could not start')
process.exitCode = 1
}
})
server.requestTimeout = 15_000
server.headersTimeout = 10_000
Starten Sie den Server aus derselben Shell, in der Sie die Einrichtung ausgeführt haben:
(
cd upload-node && corepack yarn node upload.ts
)
Lassen Sie dieses Terminal geöffnet und beenden Sie den Server anschließend mit Ctrl+C. Speichern Sie
das Formular als form.html und öffnen Sie es im Browser oder nutzen Sie die
Postman-Schritte unten. Jede akzeptierte Anfrage gibt 201 zurück und erstellt ein neues privates
Verzeichnis mit image.jpg, image.png oder image.gif; der übermittelte Dateiname wird nie als Speicherpfad verwendet.
express-fileupload übernimmt das Multipart-Parsing;
es ist nicht grundsätzlich sicherer als ein anderer gepflegter Parser.
file-type erkennt Dateisignaturen, nicht die Sicherheit einer Datei.
Eine PNG-Datei mit angehängten Skript-Bytes kann weiterhin als PNG erkannt werden. Privater Speicher
und generierte Dateiendungen verhindern, dass der Handler unter einem vom Client vorgegebenen Namen
eine über das Web ausführbare Datei erstellt. Die sichere öffentliche Auslieferung von Bildern braucht
eine separate Richtlinie für Decodierung, erneutes Encoding und Sicherheits-Scans.
Datei-Uploads in verschiedenen Frameworks verarbeiten
Python (Flask)
Getestet wurde die Python-Laufzeit 3.12.3 mit Flask 3.1.3 und Pillow 12.3.0. Das Python-Modul venv
und pip müssen installiert sein. Verwenden Sie in einem neuen Arbeitsverzeichnis ein separates Projekt
und aktivieren Sie dessen venv nicht in der aufrufenden Shell:
(
command -v python3 >/dev/null || { printf 'Missing python3\n' >&2; exit 1; }
mkdir upload-flask && cd upload-flask &&
python3 -m venv .venv &&
.venv/bin/python -m pip install Flask==3.1.3 Pillow==12.3.0
)
Speichern Sie den Code als upload-flask/app.py. Der vollständige Handler erstellt privaten Speicher,
prüft das Bild mit einem tatsächlichen Bildparser und verwendet ausschließlich eine serverseitig gewählte
Dateiendung. Das Anfragelimit schließt den Multipart-Overhead ein; das Dateilimit wird separat geprüft.
from io import BytesIO
from pathlib import Path
import os
import uuid
import warnings
from flask import Flask, abort, request
from PIL import Image, UnidentifiedImageError
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024
# Keep Flask's default form-memory limit; Werkzeug also applies it to parser buffers.
app.config['MAX_FORM_PARTS'] = 2
root = Path.cwd() / 'private-uploads'
root.mkdir(mode=0o700, exist_ok=True)
Image.MAX_IMAGE_PIXELS = 20_000_000
extensions = {'JPEG': 'jpg', 'PNG': 'png', 'GIF': 'gif'}
@app.post('/upload')
def upload():
if set(request.files) != {'myfile'} or len(request.files.getlist('myfile')) != 1:
abort(400)
data = request.files['myfile'].stream.read(5 * 1024 * 1024 + 1)
if not data or len(data) > 5 * 1024 * 1024:
abort(413)
try:
with warnings.catch_warnings():
warnings.simplefilter('error', Image.DecompressionBombWarning)
with Image.open(BytesIO(data), formats=['JPEG', 'PNG', 'GIF']) as image:
extension = extensions.get(image.format)
image.verify()
except (UnidentifiedImageError, OSError, SyntaxError, ValueError,
Image.DecompressionBombError, Image.DecompressionBombWarning):
abort(400)
if extension is None:
abort(400)
destination = root / (uuid.uuid4().hex + '.' + extension)
created = False
try:
descriptor = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
created = True
with os.fdopen(descriptor, 'wb') as output:
output.write(data)
except OSError:
if created:
destination.unlink(missing_ok=True)
abort(500)
return 'File uploaded successfully.', 201
@app.errorhandler(500)
def storage_error(_error):
return 'Upload failed.', 500
if __name__ == '__main__':
app.run(host='127.0.0.1', port=int(os.environ.get('PORT', '3000')), debug=False)
Starten Sie ihn aus der ursprünglichen Shell und beenden Sie ihn mit Ctrl+C:
(
cd upload-flask && .venv/bin/python app.py
)
Falls pip nach dem Erstellen der venv gescheitert ist, behalten Sie diese und versuchen Sie es erneut aus der ursprünglichen Shell:
(
cd upload-flask && .venv/bin/python -m pip install Flask==3.1.3 Pillow==12.3.0
)
Der integrierte Server ist für lokale Entwicklung gedacht. Verwenden Sie dasselbe Formular und das
Feld myfile wie bei Node.
Die Verifikation mit Pillow
codiert das Bild nicht erneut und entfernt keine angehängten Inhalte. Speichern Sie Originale privat;
isolieren Sie die Bildverarbeitung und setzen Sie CPU- und Arbeitsspeicherlimits, bevor Sie nicht
vertrauenswürdige öffentliche Anfragen annehmen.
PHP
Dieses Beispiel wurde mit PHP 8.4.26 und seiner FileInfo-Erweiterung getestet. Erstellen Sie zuerst die Verzeichnisse:
(
command -v php >/dev/null || { printf 'Missing php\n' >&2; exit 1; }
php -r 'exit(extension_loaded("fileinfo") ? 0 : 1);' &&
mkdir upload-php && cd upload-php &&
mkdir public && mkdir -m 700 private-uploads
)
Speichern Sie den Handler als upload-php/public/upload.php. Starten Sie ihn aus der ursprünglichen Shell
und beenden Sie ihn mit Ctrl+C:
(
cd upload-php &&
php -d display_errors=0 -d upload_max_filesize=5M -d post_max_size=6M -d max_file_uploads=2 -S 127.0.0.1:3000 -t public
)
Setzen Sie die Formularaktion oder die Postman-URL auf
http://127.0.0.1:3000/upload.php. PHP muss Eigentümer des privaten Verzeichnisses sein und dort schreiben können.
Die Upload-Vorgaben von PHP umfassen einen
Upload-Fehlercode und einen temporären Pfad auf dem Server. Prüfen Sie diese, bevor Sie den Inhalt untersuchen.
Übernehmen Sie nie die übermittelte Dateiendung: Eine Datei namens picture.php kann Bilddaten enthalten.
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
function rejectUpload(int $status): never {
http_response_code($status);
exit('Upload rejected.');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST');
rejectUpload(405);
}
if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 6 * 1024 * 1024) {
rejectUpload(413);
}
$file = $_FILES['myfile'] ?? null;
if (count($_FILES) !== 1 || !is_array($file) ||
!isset($file['error']) || !is_int($file['error'])) {
rejectUpload(400);
}
if ($file['error'] === UPLOAD_ERR_INI_SIZE || $file['error'] === UPLOAD_ERR_FORM_SIZE) {
rejectUpload(413);
}
if ($file['error'] !== UPLOAD_ERR_OK || !is_string($file['tmp_name'] ?? null) ||
!is_uploaded_file($file['tmp_name'])) {
rejectUpload(400);
}
$directory = null;
try {
$size = filesize($file['tmp_name']);
if ($size === false || $size < 1 || $size > 5 * 1024 * 1024) {
rejectUpload(413);
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
$extensions = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif'];
if (!is_string($mime) || !isset($extensions[$mime])) {
rejectUpload(400);
}
$root = dirname(__DIR__) . '/private-uploads';
if (!is_dir($root) || !is_writable($root)) {
throw new RuntimeException('Storage unavailable');
}
$candidate = $root . '/' . bin2hex(random_bytes(16));
if (!mkdir($candidate, 0700)) {
throw new RuntimeException('Storage unavailable');
}
$directory = $candidate;
$destination = $directory . '/image.' . $extensions[$mime];
if (!move_uploaded_file($file['tmp_name'], $destination) || !chmod($destination, 0600)) {
throw new RuntimeException('Storage unavailable');
}
http_response_code(201);
echo 'File uploaded successfully.';
} catch (Throwable $error) {
if ($directory !== null) {
if (isset($destination) && is_file($destination)) {
unlink($destination);
}
rmdir($directory);
}
error_log('Upload storage failed');
http_response_code(500);
echo 'Upload failed.';
}
Deaktivieren Sie display_errors bei einer HTTP-Bereitstellung, damit PHP-Dateisystemwarnungen
keine Pfade offenlegen können. FileInfo erkennt Inhaltstypen, ist aber weder ein Malware-Scanner noch
ein vollständiger Bilddecoder. PHP entfernt seine temporären Upload-Dateien am Ende der Anfrage;
der Handler verschiebt nur akzeptierte Dateien in den privaten Speicher.
Das zufällige private Unterverzeichnis verhindert, dass andere akzeptierte Uploads
durch move_uploaded_file() überschrieben werden.
Ruby on Rails
Die Validatoren attached, content_type und size stammen aus
active_storage_validations, nicht aus
Rails selbst. Dieses Beispiel verwendet Rails 8.1.3.1, die getestete Ruby-Laufzeit 3.3.10 und Version
4.1.1 dieses Gems. Fügen Sie es der Gemfile Ihrer bestehenden Anwendung hinzu und führen Sie
bundle install aus:
gem 'active_storage_validations', '4.1.1'
Halten Sie die aufgelösten Abhängigkeiten Ihrer Anwendung in Gemfile.lock fest.
Verwenden Sie eine bestehende Anwendung mit Authentifizierung und installiertem Active Storage.
Konfigurieren Sie einen privaten Disk-Dienst oder privaten Objektspeicher anhand des
Active-Storage-Leitfadens.
Legen Sie bei einem lokalen Disk-Dienst dessen Stammverzeichnis außerhalb von public ab
und geben Sie diesem Verzeichnis den Modus 0700. Die folgende Option zur Erkennung vorgetäuschter
Inhaltstypen benötigt zusätzlich das UNIX-Programm file. Getestet wurden die Hilfsprogramme
file 5.46 und 5.48; ihre PNG-Klassifizierung unterscheidet sich, wie die Annahmetabelle
unten beschreibt. Dieser Host verwendete Rack 3.2.7. Installieren Sie das Hilfsprogramm separat von den
Ruby-Gems.
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :myfile
validates :myfile,
content_type: { in: ['image/png', 'image/jpeg', 'image/gif'], spoofing_protection: true },
size: { between: 1..5.megabytes }
end
Verwenden Sie diesen Controller in einer Anwendung, deren Authentifizierungsschicht bereits
current_user bereitstellt. Wählen Sie keinen Nutzer aus params[:user_id] aus. Er akzeptiert nur
einen neuen Multipart-Upload, keine vom Client übermittelte signierte Blob-ID von Active Storage.
# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
before_action :require_upload_user
protect_from_forgery with: :exception
def new
render :new
end
def create
file = params.require(:user).permit(:myfile)[:myfile]
unless file.is_a?(ActionDispatch::Http::UploadedFile)
return render plain: 'Send a file.', status: :bad_request
end
user = current_user
if user.update(myfile: file)
render plain: 'File uploaded successfully.', status: :created
else
reasons = user.errors.details[:myfile].map { |detail| detail[:error].to_s }.uniq
Rails.logger.warn('Upload validation rejected: ' + reasons.join(', '))
render plain: 'File rejected.', status: :unprocessable_entity
end
end
private
def require_upload_user
head :unauthorized unless current_user
end
end
Fügen Sie diese Routen zu config/routes.rb hinzu:
get '/upload', to: 'uploads#new'
post '/upload', to: 'uploads#create'
Speichern Sie diese Ansicht als app/views/uploads/new.html.erb. Rails erzeugt das CSRF-Token und den
verschachtelten Feldnamen user[myfile], den der Controller erwartet:
<%= form_with scope: :user, url: '/upload', multipart: true do |form| %>
<%= form.label :myfile, 'Image' %>
<%= form.file_field :myfile, accept: 'image/png,image/jpeg,image/gif', required: true %>
<%= form.submit 'Upload file' %>
<% end %>
Starten Sie die bestehende Anwendung aus ihrem Projektverzeichnis:
bin/rails server -b 127.0.0.1 -p 3000
Melden Sie sich über die normale Anmeldung an und öffnen Sie /upload. Beenden Sie den lokalen
Server mit Ctrl+C. current_user muss das angemeldete Objekt vom Typ
User zurückgeben; Anhänge bleiben bei dieser Funktion optional, damit das Erstellen eines
Nutzers oder das Bearbeiten seines Namens weiterhin funktioniert.
Halten Sie den Speicherdienst privat und konfigurieren Sie Download-Controller mit Authentifizierung. Die standardmäßigen signierten URLs von Active Storage ersetzen keine nutzerspezifische Autorisierung. Setzen Sie auf Ihrem Webserver ein Limit für den Anfragekörper durch, bevor Rails Multipart-Daten parst. Die Modellvalidierung erfolgt nach dem Parsing und kann dieses Limit für Netzwerkressourcen nicht bereitstellen. Diese Beispiele aktivieren keine direkten Uploads; ungenutzte Blobs aus anderen Abläufen benötigen eine separate Aufbewahrungsrichtlinie.
Häufige Probleme bei Datei-Uploads und ihre Lösungen
Warum lässt sich meine Datei nicht hochladen?
Beginnen Sie mit der Antwort und prüfen Sie dann, in welcher Phase die Anfrage abgewiesen wurde. Alle diese lokalen Handler verlangen eine nicht leere Datei von höchstens 5 MiB (5.242.880 Bytes), einschließlich genau 5 MiB. Node, Flask und PHP begrenzen außerdem den deklarierten Anfragekörper auf 6 MiB, einschließlich Multipart-Overhead. Bei Rails muss Ihr bestehender Webserver dieses Limit für den Anfragekörper vor der Modellvalidierung durchsetzen.
| Symptom | Was Sie in diesen Beispielen prüfen sollten |
|---|---|
| 404 oder 405 | Prüfen Sie die URL und die POST-Methode. PHP empfängt /upload.php; die anderen Beispiele empfangen /upload. |
| 400 | Prüfen Sie die Multipart-Kodierung des Anfragekörpers, das erwartete Feld und die folgende Richtlinie zur Inhaltserkennung. Flask gibt auch bei Überschreitung seines Pixellimits oder fehlgeschlagener Bildverifikation 400 zurück. Nach einem Inaktivitäts-Timeout beim Upload kann der Node-Parser keine Datei liefern; daraus entsteht derselbe Status 400 wie bei einem fehlenden Feld. |
| 411 bei Node | Senden Sie eine bekannte Länge des Anfragekörpers. Dieses Beispiel weist Streaming-Anfragen mit Chunked-Übertragung ab. |
| 413 | Prüfen Sie die Bytelimits für Datei und Anfrage. Node, Flask und PHP verwenden 413 auch für eine leere Datei. Der Flask-Parser kann bei zu vielen Teilen oder beim Erreichen seines Arbeitsspeicherlimits für Formulardaten 413 zurückgeben. |
| 401 bei Rails | Melden Sie sich über die bestehende Anwendung an. Ein Upload kann keinen Nutzer anhand einer ID auswählen. |
| 422 bei Rails | Übermitteln Sie das CSRF-Token der Sitzung und das verschachtelte Dateifeld user[myfile]. Auch eine Anfrage mit gültigem CSRF-Token kann an der Inhalts- oder Größenvalidierung scheitern, etwa bei einer leeren Datei. |
| 500 | Prüfen Sie die Schreibrechte für den Speicher. Der getestete Rails-Host gab auch beim Erreichen der Datei- oder Teilelimits von Rack 500 zurück, bevor der Handler ausgeführt wurde. |
| 503 bei Node | Zwei HTTP-Antworten sind bereits aktiv. Warten Sie, bis sie abgeschlossen sind, und versuchen Sie es erneut. |
Eine kleine PNG-Datei kann ein Pixellimit überschreiten und zugleich unter dem Bytelimit bleiben.
Flask weist Bilder mit mehr als 20.000.000 Pixeln ab; genau diese Pixelzahl besteht die Dimensionsprüfung.
Der Aufruf von verify() prüft die Dateistruktur, ohne die Pixel vollständig zu decodieren.
Auch die Signaturerkennung von Node, FileInfo von PHP und die MIME-Spoofing-Prüfung von Rails belegen
nicht, dass jedes Bild decodiert werden kann, dass Metadaten entfernt werden oder dass eine Datei frei
von Malware ist.
Mit den angegebenen festen Abhängigkeitsversionen wurden die folgenden Erkennungsgrenzen beobachtet. „Akzeptiert“ bedeutet, dass die ursprünglichen Bytes privat gespeichert wurden, nicht dass sie für die öffentliche Auslieferung freigegeben sind:
| Eingabe | Node | Flask | PHP 8.4.26 FileInfo | Rails mit file 5.46 / 5.48 |
|---|---|---|---|---|
| Normales PNG, JPEG mit Metadaten, GIF | Akzeptiert | Akzeptiert | Akzeptiert | Akzeptiert / akzeptiert |
| Gültiges MPF/MPO-JPEG mit mehreren Bildern | Als JPEG akzeptiert | 400: Pillow meldet MPO, außerhalb der Positivliste für zurückgegebene Formate | Als JPEG akzeptiert | Akzeptiert / akzeptiert |
| Kompaktes APNG | 400: als APNG erkannt | Als PNG akzeptiert | Als PNG akzeptiert | Akzeptiert / 422: MIME-Abweichung |
| APNG mit 12 KB Metadaten vor seinem Animationsmarker | Als PNG akzeptiert | Als PNG akzeptiert | Als PNG akzeptiert | Akzeptiert / 422: MIME-Abweichung |
| APNG mit 40 KB oder 90 KB vor diesem Marker | Als PNG akzeptiert | Als PNG akzeptiert | Als PNG akzeptiert | Akzeptiert / akzeptiert |
Node untersucht nur die ersten 4.100 Bytes. Bei file 5.48 sucht die
PNG-Erkennungsregel
ab Offset 33 in 16.384 Bytes nach dem Animationsmarker. Wird ein Marker hinter das Suchfenster
verschoben, ändert sich die Erkennung, ohne dass sich ändert, ob das Bild animiert ist. Diese Handler
setzen kein allgemeines Animationsverbot durch. Auch die Ablehnung von MPO durch Flask ist eine Grenze
der Annahmerichtlinie, kein Beweis für ein beschädigtes JPEG; siehe MPO-Unterstützung in Pillow.
Die Anzahl der Multipart-Teile birgt eine weitere Falle: Geparste Felder entsprechen nicht der Anzahl
aller übermittelten Teile. Node untersucht höchstens zwei Dateiteile und insgesamt drei Teile,
verwirft Textfelder und weist mehrere Dateien ab, die in seiner geparsten Zuordnung sichtbar sind.
Eine fremde Datei jenseits des Teilelimits des Parsers kann ausgelassen werden, während das sichtbare
Feld myfile mit 201 gespeichert wird. Flask erlaubt insgesamt zwei Teile, zählt auch Textteile
und gibt oberhalb dieses Limits 413 zurück; doppelte sichtbare Dateien führen zu 400. Sein
standardmäßiges Arbeitsspeicherlimit für Formulardaten bleibt bei 500.000 Bytes, unabhängig von den
Datei- und Anfragekörperlimits.
Die angegebene PHP-Einstellung max_file_uploads=2 verarbeitet bis zu zwei Dateiteile. Doppelte skalare
Teile namens myfile werden auf den zuletzt verarbeiteten reduziert; spätere Dateiteile können
ignoriert werden. Rails übernimmt ebenfalls das zuletzt geparste Feld user[myfile] und ignoriert
andere Felder, die der Controller von seiner Eingabe ausschließt. Auf dem getesteten Rack-Host wurden
127 Dateiteile und insgesamt 4.095 Teile akzeptiert; bei 128 Dateiteilen oder insgesamt 4.096 Teilen
trat eine Parser-Ausnahme auf. Diese begrenzten Richtlinien für geparste Felder garantieren nicht, dass
jede rohe Dopplung oder zusätzliche Datei abgewiesen wird. Senden Sie für diese Aufgabe eine Datei
und keine unnötigen Felder.
Lösungen
Verwenden Sie zuerst ein bekanntes, decodierbares Bild und ändern Sie dann jeweils nur eine Eingabe: den Feldnamen, die Größe des Anfragekörpers, den Inhalt oder die Zugriffsrechte am Ziel. Node hat zusätzlich einen Inaktivitäts-Timer für Uploads von zehn Sekunden und ein HTTP-Anfrage-Timeout von 15 Sekunden. Eine langsame Anfrage kann unterhalb des Größenlimits scheitern; diese Einstellungen garantieren keine Antwort nach einer exakt festgelegten tatsächlichen Zeitspanne.
Bei Validierungsfehlern in Rails protokolliert der gezeigte Controller nur Begründungscodes aus
errors.details,
unter dem Präfix „Upload validation rejected“. Sehen Sie dort nach einem Status 422 nach, um Ablehnungen
aufgrund von Inhaltstyp, Spoofing und Größe zu unterscheiden. Protokollieren Sie keine übermittelten
Bytes, Dateinamen, Sitzungscookies oder Tokens.
Prüfen Sie den Zustand in Rails, bevor Sie einen Vorgang nach einem Speicherfehler erneut versuchen. Bei einer Ablehnung durch die Validierung bleiben die bisherige Anhangreferenz und die Bytes erhalten. Ein späterer Schreibfehler verhält sich anders: Active Storage speichert die Referenz vor dem Upload nach dem Commit. Beim getesteten Disk-Dienst mit dem Inline-Job-Adapter führte ein erzwungener Schreibfehler zu 500 und einer neuen, bereits festgeschriebenen Referenz auf eine fehlende Datei. Der vorherige Blob und seine Bytes blieben erhalten, waren aber nicht mehr der aktuelle Anhang des Nutzers. Lesen Sie den Nutzer mit einer neuen Abfrage und prüfen Sie die tatsächliche Datei oder den Download dieses Blobs. Untersuchen Sie anschließend den vorherigen Blob und Ihre Job-Queue. Ein Status 500 belegt keinen Rollback; beheben Sie die Speicherursache und gleichen Sie die Referenz ab, bevor Sie erneut aktualisieren.
Datei-Upload-APIs mit Postman testen
Verwenden Sie zuerst die laufende Node- oder Flask-Demonstration. Diese Schritte wurden im
Linux-Desktop-Client von Postman 12.31.7 getestet; PHP nutzt dasselbe Feld mit seiner URL
/upload.php. Verwenden Sie für Rails das von der Anwendung generierte Formular, sofern Sie nicht
auch ihre authentifizierte Sitzung und ihr CSRF-Token übermitteln.
- Erstellen Sie eine POST-Anfrage an
http://127.0.0.1:3000/upload. - Öffnen Sie Body und wählen Sie form-data.
- Fügen Sie
myfilehinzu, wählen Sie File als Typ und wählen Sie ein lokales Bild im Dateiauswahldialog des Betriebssystems. - Überlassen Sie Postman den Multipart-Header Content-Type, damit dieser die passende Boundary enthält.
- Klicken Sie auf Send. Erwarten Sie 201 und eine neue gespeicherte Datei; vergleichen Sie dann deren Bytes mit dem ausgewählten Bild.
- Ändern Sie nur das Feld in
wrongfieldund senden Sie erneut. Node, Flask und PHP geben 400 zurück, ohne eine gespeicherte Datei hinzuzufügen.
Für eine Prüfung über die Befehlszeile legen Sie die ausgewählte gültige PNG-Datei unter
sample.png im Verzeichnis der aufrufenden Shell ab.
Verwenden Sie cURL mit Unterstützung für --fail-with-body; dieser Befehl wurde mit cURL 8.5.0 getestet:
curl --fail-with-body --silent --show-error -F "myfile=@./sample.png" http://127.0.0.1:3000/upload
Eine erfolgreiche Antwort besagt, dass der Handler seinen Schreibvorgang abgeschlossen hat. Prüfen Sie
auch private-uploads: Vergleichen Sie die gesamten gespeicherten Bytes und öffnen Sie das gespeicherte
Bild mit einem unabhängigen Decoder. Eine Antwort allein kann Ihnen nicht sagen, ob Sie die richtige
Ausgabe geprüft haben. Die Demonstrationen behalten akzeptierte Originale; beenden Sie anschließend
den Server und entfernen Sie nur Ihre eigenen hochgeladenen Fixtures.
Bewährte Verfahren für sichere Datei-Uploads
Speichern Sie Originale außerhalb statischer Verzeichnisse und Dokumentenstammverzeichnisse. Die
Node- und PHP-Beispiele verwenden für jeden Upload ein neues privates Unterverzeichnis mit einem
serverseitig gewählten Namen; Flask erstellt eine zufällig serverseitig benannte Datei exklusiv.
Wiederholte akzeptierte Anfragen behalten separate Ausgaben. Keines der Beispiele verwendet den
übermittelten Dateinamen als Speicherpfad. Wenn Sie also ein Foto in photo.php umbenennen,
entsteht keine PHP-Skriptdatei unter diesem Namen.
Erkennung, Decodierung, Malware-Scans und Veröffentlichung sind unterschiedliche Entscheidungen. Ein erkennbarer Header kann trotz einer Beschädigung am Ende eines Bildes erhalten bleiben; die Node- und PHP-Beispiele können eine solche Datei speichern. Auch die Verifikation mit Pillow ist keine vollständige Decodierung. Verwenden Sie vor der öffentlichen Auslieferung eine isolierte Verarbeitungsphase, die unterstützte Inhalte vollständig decodiert, Ressourcenlimits durchsetzt, die vorgesehene Ausgabe erzeugt und Ihrer Richtlinie für Sicherheits-Scans folgt. Halten Sie das Original privat, bis diese Schritte abgeschlossen sind. Diese lokalen Handler implementieren weder einen Scanner noch eine Veröffentlichungsphase.
Fortgeschrittene Techniken für Datei-Uploads
Wenn Ihr eigentliches Problem das Fortsetzen unterbrochener Übertragungen oder die Anzeige des Upload-Fortschritts ist, lesen Sie weiter unter Datei-Uploads in Webanwendungen optimieren. Dort gelten andere Vorgaben für Client und Gateway; der beschriebene Client ist kein direkt kompatibler Ersatz für diese Multipart-Endpunkte. Verwenden Sie für eine Integration von Laravel und Vue mit sitzungsbasierter Authentifizierung den Laravel-Upload-Leitfaden.
Fazit und weiterführende Ressourcen
Bewahren Sie bei einem fehlgeschlagenen Multipart-Upload eine minimale erfolgreiche Anfrage auf und vergleichen Sie jeweils eine geänderte Bedingung damit. Der Flask-Upload-Leitfaden, die PHP-Upload-Dokumentation und der Active-Storage-Leitfaden erklären die nativen Vorgaben hinter diesen Beispielen.
