Datei-Uploads gehören zu den häufigsten Anforderungen in modernen Webanwendungen. Ob Profilbilder, PDF-Dokumente oder CSV-Importe: Nahezu jede interaktive Website benötigt die Möglichkeit, Dateien vom Client an den Server zu übertragen. PHP bietet mit dem superglobalen Array $_FILES und der Funktion move_uploaded_file() alle Werkzeuge, um hochgeladene Dateien serverseitig entgegenzunehmen und zu speichern. Dabei spielt Sicherheit eine zentrale Rolle, denn ein ungeschützter Upload-Endpunkt kann Angreifern Zugang zum Server verschaffen. Schädliche Dateien, manipulierte Dateinamen oder übergroße Uploads stellen reale Bedrohungen dar, die durch serverseitige Validierung abgefangen werden müssen. Dieses Tutorial erklärt den gesamten Prozess vom HTML-Formular über die Verarbeitung mit $_FILES, die Validierung von Dateityp und Dateigröße bis hin zum Upload mehrerer Dateien und der richtigen Konfiguration der php.ini.

Der Ausgangspunkt jedes Datei-Uploads ist das HTML-Formular, das die Datei an den Server sendet.
Das HTML-Formular für den Upload
Bevor PHP eine Datei verarbeiten kann, muss ein HTML-Formular die Datei an den Server senden. Entscheidend ist dabei das Attribut enctype="multipart/form-data", ohne das der Browser keine Dateidaten übermittelt.
<form method="POST" enctype="multipart/form-data" action="upload.php">
<label for="datei">Datei auswählen:</label>
<input type="file" name="datei" id="datei" accept=".jpg,.png,.gif">
<button type="submit">Hochladen</button>
</form>
Das Attribut method="POST" ist Pflicht, da Datei-Uploads über GET nicht möglich sind. Das accept-Attribut im input-Element schränkt die Dateiauswahl im Browser ein, ersetzt aber keinesfalls eine serverseitige Prüfung, da es sich clientseitig umgehen lässt. Wird enctype="multipart/form-data" vergessen, bleibt das Array $_FILES auf dem Server leer und der Upload schlägt ohne aussagekräftige Fehlermeldung fehl. Dieser Fehler zählt zu den häufigsten Stolperfallen beim Einstieg in PHP File Uploads. Das action-Attribut verweist auf das PHP-Skript, das die hochgeladene Datei entgegennimmt und verarbeitet.
Die Datei auf dem Server verarbeiten: $_FILES
Nach dem Absenden des Formulars stellt PHP alle Informationen zur hochgeladenen Datei im superglobalen Array $_FILES bereit. Jedes hochgeladene File erhält darin einen eigenen Eintrag mit fünf Eigenschaften.
<?php
/* Struktur von $_FILES nach einem Upload */
print_r($_FILES['datei']);
/*
Array
(
[name] => foto.jpg
[type] => image/jpeg
[tmp_name] => /tmp/phpYzdqkD
[error] => 0
[size] => 123456
)
*/
Der Schlüssel name enthält den ursprünglichen Dateinamen vom Client, so wie ihn der Benutzer auf seinem Rechner benannt hat. Der Wert type gibt den vom Browser gemeldeten MIME-Type an, ist aber nicht vertrauenswürdig und darf nicht als alleinige Grundlage für Sicherheitsprüfungen dienen, da der Browser diesen Wert selbst setzt und ein Angreifer ihn beliebig manipulieren kann. Unter tmp_name speichert PHP die Datei vorübergehend in einem temporären Verzeichnis auf dem Server. Der Wert error enthält einen ganzzahligen Fehlercode, wobei die Konstante UPLOAD_ERR_OK (Wert 0) einen erfolgreichen Upload signalisiert. Jeder andere Wert weist auf ein Problem hin, das gezielt ausgewertet werden sollte. Die Eigenschaft size gibt die Dateigröße in Bytes an und eignet sich für eine serverseitige Prüfung der maximalen Uploadgröße. Das temporäre File unter tmp_name wird am Ende des PHP-Skripts automatisch gelöscht, wenn es nicht vorher an einen dauerhaften Speicherort verschoben wird.
Die Datei speichern mit move_uploaded_file()
Um eine hochgeladene Datei dauerhaft zu speichern, kommt die Funktion move_uploaded_file() zum Einsatz. Sie verschiebt das temporäre File an den gewünschten Zielort und prüft intern, ob die Datei tatsächlich per HTTP-Upload hochgeladen wurde.
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
$datei = $_FILES['datei'] ?? null;
if ($datei === null || $datei['error'] !== UPLOAD_ERR_OK) {
echo "Upload-Fehler aufgetreten.";
exit;
}
$endung = pathinfo($datei['name'], PATHINFO_EXTENSION);
$neuerName = bin2hex(random_bytes(16)) . '.' . $endung;
$zielPfad = __DIR__ . '/uploads/' . $neuerName;
if (move_uploaded_file($datei['tmp_name'], $zielPfad)) {
echo "Datei erfolgreich hochgeladen: " . $neuerName;
} else {
echo "Fehler beim Speichern der Datei.";
}
Der ursprüngliche Dateiname vom Benutzer sollte niemals direkt als Zielname verwendet werden. Er kann Sonderzeichen, Leerzeichen oder sogar Pfad-Traversal-Sequenzen wie ../../ enthalten, die ein Sicherheitsrisiko darstellen. Stattdessen erzeugt bin2hex(random_bytes(16)) einen zufälligen, eindeutigen Dateinamen. Das Zielverzeichnis muss vorab existieren und Schreibrechte für den Webserver besitzen. Die Funktion move_uploaded_file() gibt true bei Erfolg und false bei einem Fehler zurück. Im Gegensatz zu rename() oder copy() prüft move_uploaded_file() intern, ob die Quelldatei tatsächlich durch einen HTTP-POST-Upload erstellt wurde. Dadurch wird verhindert, dass ein Angreifer über manipulierte tmp_name-Werte auf beliebige Serverdateien zugreift.
Sicherheitsprüfungen für den Upload
Ein Datei-Upload ohne serverseitige Validierung stellt ein erhebliches Sicherheitsrisiko dar. Die folgenden Prüfungen sollten bei jedem Upload durchgeführt werden.
Das folgende Diagramm zeigt den empfohlenen Ablauf der Sicherheitsprüfungen bei einem PHP File Upload.
flowchart TD
A["Datei hochgeladen"] --> B{"Upload-Fehler?"}
B -->|Ja| C["Fehlermeldung ausgeben"]
B -->|Nein| D{"MIME-Type erlaubt?"}
D -->|Nein| E["Abgelehnt: falscher Typ"]
D -->|Ja| F{"Dateigröße ok?"}
F -->|Nein| G["Abgelehnt: zu groß"]
F -->|Ja| H["Dateinamen generieren"]
H --> I["move_uploaded_file()"]
I --> J["Datei gespeichert"]
Dateityp prüfen (MIME-Type und Dateiendung)
Der vom Browser gesendete MIME-Type in $_FILES['datei']['type'] lässt sich leicht manipulieren. Eine zuverlässige Prüfung verwendet stattdessen die Funktion mime_content_type() oder die finfo-Erweiterung, die den tatsächlichen Inhalt der Datei analysiert.
<?php
$datei = $_FILES['datei'];
$erlaubteTypen = ['image/jpeg', 'image/png', 'image/gif'];
/* MIME-Type serverseitig ermitteln */
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($datei['tmp_name']);
if (!in_array($mimeType, $erlaubteTypen, true)) {
echo "Dateityp nicht erlaubt: " . $mimeType;
exit;
}
/* Zusätzlich die Dateiendung prüfen */
$erlaubteEndungen = ['jpg', 'jpeg', 'png', 'gif'];
$endung = strtolower(pathinfo($datei['name'], PATHINFO_EXTENSION));
if (!in_array($endung, $erlaubteEndungen, true)) {
echo "Dateiendung nicht erlaubt.";
exit;
}
echo "Dateityp und Endung sind gültig.";
Die Kombination aus MIME-Type-Prüfung und Endungsvalidierung bietet doppelten Schutz. Eine Datei mit der Endung .php, die als Bild getarnt ist, wird so zuverlässig erkannt und abgelehnt. Der dritte Parameter true bei in_array() erzwingt einen strikten Typenvergleich und verhindert unerwartete Typumwandlungen.
Dateigröße begrenzen
Neben dem Dateityp muss auch die Dateigröße serverseitig geprüft werden. Die Angabe size in $_FILES enthält die Größe in Bytes.
<?php
$datei = $_FILES['datei'];
$maxGroesse = 5 * 1024 * 1024; /* 5 MB in Bytes */
if ($datei['size'] > $maxGroesse) {
echo "Die Datei ist zu groß. Maximal erlaubt: 5 MB.";
exit;
}
if ($datei['size'] === 0) {
echo "Die Datei ist leer.";
exit;
}
echo "Dateigröße ist akzeptabel: " . round($datei['size'] / 1024, 2) . " KB";
Die serverseitige Prüfung ist unverzichtbar, da Einstellungen in der php.ini (wie upload_max_filesize) den Upload zwar begrenzen, aber bei Überschreitung keinen benutzerfreundlichen Hinweis liefern. Die Kombination aus php.ini-Limit und expliziter Prüfung im PHP-Skript sorgt für zuverlässige Kontrolle und verständliche Rückmeldungen.
Dateinamen bereinigen und ausführbaren Code verhindern
Benutzerdefinierte Dateinamen können gefährliche Zeichen oder Pfadangaben enthalten. Besonders kritisch sind Dateien mit ausführbaren Endungen wie .php, .phtml oder .phar, die bei einem Aufruf über den Webserver serverseitigen Code ausführen können.
<?php
$datei = $_FILES['datei'];
/* Gefährliche Endungen blockieren */
$verboteneEndungen = ['php', 'phtml', 'phar', 'php3', 'php4', 'php5'];
$endung = strtolower(pathinfo($datei['name'], PATHINFO_EXTENSION));
if (in_array($endung, $verboteneEndungen, true)) {
echo "Ausführbare Dateien sind nicht erlaubt.";
exit;
}
/* Sicheren Dateinamen generieren */
$sichererName = bin2hex(random_bytes(16)) . '.' . $endung;
/* Upload-Verzeichnis außerhalb des Web-Root verwenden */
$zielPfad = '/var/uploads/' . $sichererName;
if (move_uploaded_file($datei['tmp_name'], $zielPfad)) {
echo "Datei sicher gespeichert als: " . $sichererName;
}
Das Speichern der Dateien außerhalb des öffentlich zugänglichen Web-Root-Verzeichnisses bietet zusätzlichen Schutz. So kann ein Angreifer selbst bei einem erfolgreichen Upload einer manipulierten Datei diese nicht direkt über eine URL im Browser aufrufen. Die Auslieferung der Dateien erfolgt dann über ein separates PHP-Skript, das Zugriffsrechte prüft.
Upload-Fehler behandeln
PHP definiert mehrere Fehlerkonstanten, die in $_FILES['datei']['error'] zurückgegeben werden. Eine vollständige Fehlerbehandlung wertet diese Konstanten aus und gibt dem Benutzer eine verständliche Rückmeldung.
<?php
$fehlerMeldungen = [
UPLOAD_ERR_INI_SIZE => 'Die Datei überschreitet upload_max_filesize.',
UPLOAD_ERR_FORM_SIZE => 'Die Datei überschreitet MAX_FILE_SIZE.',
UPLOAD_ERR_PARTIAL => 'Die Datei wurde nur teilweise hochgeladen.',
UPLOAD_ERR_NO_FILE => 'Es wurde keine Datei ausgewählt.',
UPLOAD_ERR_NO_TMP_DIR => 'Das temporäre Verzeichnis fehlt.',
UPLOAD_ERR_CANT_WRITE => 'Die Datei konnte nicht geschrieben werden.',
UPLOAD_ERR_EXTENSION => 'Eine PHP-Erweiterung hat den Upload gestoppt.',
];
$fehlerCode = $_FILES['datei']['error'];
if ($fehlerCode === UPLOAD_ERR_OK) {
echo "Upload erfolgreich. Verarbeitung startet.";
} else {
$meldung = $fehlerMeldungen[$fehlerCode] ?? 'Unbekannter Upload-Fehler.';
echo "Fehler: " . $meldung;
}
Die Konstante UPLOAD_ERR_INI_SIZE tritt auf, wenn die Datei die in upload_max_filesize festgelegte Grenze überschreitet. Der Fehlercode UPLOAD_ERR_PARTIAL bedeutet, dass die Verbindung während des Uploads abgebrochen wurde. Besonders tückisch ist UPLOAD_ERR_NO_TMP_DIR, da dieser Fehler auf eine Fehlkonfiguration des Servers hinweist und nicht vom Benutzer behoben werden kann. Eine saubere Fehlerbehandlung loggt solche serverseitigen Probleme intern und zeigt dem Benutzer nur eine allgemeine Meldung.
Mehrere Dateien gleichzeitig hochladen
PHP unterstützt den Upload mehrerer Dateien in einem einzigen Formular. Dafür wird das name-Attribut des input-Elements als Array notiert und das Attribut multiple hinzugefügt.
<form method="POST" enctype="multipart/form-data" action="upload.php">
<label for="dateien">Mehrere Dateien auswählen:</label>
<input type="file" name="dateien[]" id="dateien" multiple>
<button type="submit">Alle hochladen</button>
</form>
Die Struktur von $_FILES unterscheidet sich bei mehreren Dateien vom Einzel-Upload. Statt eines einzelnen Werts enthält jede Eigenschaft ein Array mit einem Eintrag pro Datei. Das folgende PHP-Skript verarbeitet alle hochgeladenen Dateien in einer Schleife.
<?php
if (empty($_FILES['dateien'])) {
echo "Keine Dateien empfangen.";
exit;
}
$erlaubteTypen = ['image/jpeg', 'image/png', 'image/gif'];
$maxGroesse = 2 * 1024 * 1024;
$anzahl = count($_FILES['dateien']['name']);
$erfolgreich = 0;
for ($i = 0; $i < $anzahl; $i++) {
/* Fehlerprüfung für jede einzelne Datei */
if ($_FILES['dateien']['error'][$i] !== UPLOAD_ERR_OK) {
continue;
}
/* MIME-Type prüfen */
$mimeType = mime_content_type($_FILES['dateien']['tmp_name'][$i]);
if (!in_array($mimeType, $erlaubteTypen, true)) {
continue;
}
/* Dateigröße prüfen */
if ($_FILES['dateien']['size'][$i] > $maxGroesse) {
continue;
}
/* Sicheren Namen erzeugen und speichern */
$endung = pathinfo($_FILES['dateien']['name'][$i], PATHINFO_EXTENSION);
$neuerName = bin2hex(random_bytes(16)) . '.' . strtolower($endung);
$zielPfad = __DIR__ . '/uploads/' . $neuerName;
if (move_uploaded_file($_FILES['dateien']['tmp_name'][$i], $zielPfad)) {
$erfolgreich++;
}
}
echo $erfolgreich . " von " . $anzahl . " Dateien erfolgreich hochgeladen.";
Bei mehreren Dateien ist es wichtig, jede Datei einzeln zu validieren. Eine ungültige Datei sollte den Upload der übrigen nicht blockieren, weshalb das Beispiel mit continue zur nächsten Datei springt. Die verschachtelte Struktur von $_FILES bei Multi-Uploads gruppiert die Daten nach Eigenschaft (name, tmp_name, error) statt nach Datei. Deshalb wird der Index $i verwendet, um auf die zusammengehörenden Werte einer einzelnen Datei zuzugreifen. Diese Struktur ist eine häufige Fehlerquelle, da viele Entwickler eine Gruppierung nach Datei erwarten, also ein Array aus einzelnen Datei-Arrays. Wer die Daten umstrukturieren möchte, kann sie in einer Schleife in ein übersichtlicheres Format überführen.
php.ini-Einstellungen für Uploads
Mehrere Direktiven in der php.ini beeinflussen das Verhalten von Datei-Uploads. Eine falsche Konfiguration kann dazu führen, dass Uploads ohne erkennbaren Fehler im PHP-Skript fehlschlagen.
<?php
/* Aktuelle Upload-Einstellungen auslesen */
$einstellungen = [
'file_uploads' => ini_get('file_uploads'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'max_file_uploads' => ini_get('max_file_uploads'),
'upload_tmp_dir' => ini_get('upload_tmp_dir'),
];
foreach ($einstellungen as $key => $value) {
echo $key . ': ' . ($value ?: '(nicht gesetzt)') . "\n";
}
/*
file_uploads: 1
upload_max_filesize: 2M
post_max_size: 8M
max_file_uploads: 20
upload_tmp_dir: (nicht gesetzt)
*/
Die Direktive file_uploads muss auf On stehen, damit Uploads überhaupt funktionieren. Der Wert upload_max_filesize legt die maximale Größe einer einzelnen Datei fest, während post_max_size die Gesamtgröße aller POST-Daten einschließlich aller Dateien begrenzt. Dabei muss post_max_size stets größer sein als upload_max_filesize. Die Einstellung max_file_uploads definiert die maximale Anzahl gleichzeitig hochladbarer Dateien. Wird upload_tmp_dir nicht gesetzt, nutzt PHP das standardmäßige temporäre Verzeichnis des Betriebssystems. Diese Werte lassen sich je nach Serverkonfiguration in der php.ini, einer .htaccess-Datei oder per ini_set() anpassen, wobei ini_set() für Upload-Direktiven nur eingeschränkt funktioniert, da sie bereits vor der Skriptausführung greifen.
Fazit
Ein sicherer PHP File Upload erfordert mehrere aufeinander abgestimmte Schritte. Das HTML-Formular benötigt zwingend das Attribut enctype="multipart/form-data", damit der Browser die Dateidaten korrekt übermittelt. Auf dem Server liefert das Array $_FILES alle relevanten Informationen zur hochgeladenen Datei, wobei der vom Browser gemeldete MIME-Type nicht vertrauenswürdig ist. Stattdessen sollte finfo oder mime_content_type() für die zuverlässige serverseitige Typprüfung eingesetzt werden. Die Funktion move_uploaded_file() stellt sicher, dass nur tatsächlich per HTTP hochgeladene Dateien verschoben werden, und bietet damit einen wichtigen Schutz gegenüber manipulierten Pfadangaben. Dateinamen vom Benutzer dürfen niemals direkt als Zielname verwendet werden, da sie Pfad-Traversal-Angriffe ermöglichen können. Zufällig generierte Dateinamen mit bin2hex(random_bytes()) schaffen hier Abhilfe. Bei mehreren Dateien ist die verschachtelte Struktur von $_FILES zu beachten, die nach Eigenschaft statt nach Datei gruppiert. Eine sorgfältige Fehlerbehandlung mit den UPLOAD_ERR_*-Konstanten sorgt für aussagekräftige Rückmeldungen an den Benutzer. Die Konfiguration in der php.ini mit upload_max_filesize, post_max_size und max_file_uploads bildet die Grundlage für funktionsfähige Uploads. Wer alle diese Punkte beachtet, kann Datei-Uploads in PHP zuverlässig und sicher umsetzen.