Unvollständige Identifizierung von Upload-Dateivariablen (PHP)
Beschreibung
Unvollständige Identifizierung von Upload-Dateivariablen tritt in PHP-Anwendungen auf, wenn Datei-Upload-Behandlungscode nicht alle Aspekte der hochgeladenen Dateidaten ordnungsgemäß validiert. PHPs $_FILES-Superglobal enthält mehrere Variablen für jeden Upload (name, type, tmp_name, error, size), und Angreifer können Client-seitige Werte manipulieren. Das Vertrauen auf den Client-bereitgestellten Dateinamen oder MIME-Typ ohne Validierung führt zu verschiedenen Angriffen.
Risiko
Angreifer können bösartige Dateien hochladen, indem sie die Dateinamenerweiterung manipulieren. Client-bereitgestellte MIME-Typen können gefälscht werden, um Inhaltsfilter zu umgehen. Path-Traversal in Dateinamen kann kritische Dateien überschreiben. Dateierweiterungsfehlanpassungen ermöglichen Code-Ausführung (PHP als Bild hochladen). Doppelte Erweiterungen und Null-Bytes können Filter umgehen. Der tmp_name könnte in bestimmten Konfigurationen potenziell manipuliert werden.
Lösung
Vertrauen Sie niemals dem Client-bereitgestellten Dateinamen oder MIME-Typ. Generieren Sie serverseitige Dateinamen. Validieren Sie Dateiinhalt mit Magic Bytes/Signaturen. Verwenden Sie Allowlists für erlaubte Erweiterungen. Prüfen Sie den tatsächlichen Dateiinhaltstyp serverseitig. Bereinigen Sie Dateinamen und entfernen Sie Pfadkomponenten. Speichern Sie Uploads außerhalb des Web-Roots. Verwenden Sie separate Domains für Benutzerinhalte.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Dateiüberschreibung Path-Traversal kann Systemdateien überschreiben. |
| Vertraulichkeit | Bereich: Code-Ausführung Hochladen von PHP-Dateien führt zu Remote-Code-Execution. |
| Sicherheit | Bereich: Inhalts-Injection Bösartige Dateien werden Benutzern bereitgestellt. |
Beispielcode + Lösungscode
Verwundbarer Code
<?php
// VERWUNDBAR: Client-bereitgestelltem Dateinamen vertrauen
function uploadVulnerable1() {
if (isset($_FILES['upload'])) {
$filename = $_FILES['upload']['name']; // Client-kontrolliert!
$dest = "/var/www/uploads/" . $filename;
// Angreifer: filename = "../../config.php" (Path-Traversal)
// Angreifer: filename = "shell.php" (Code-Execution)
move_uploaded_file($_FILES['upload']['tmp_name'], $dest);
}
}
// VERWUNDBAR: Client-bereitgestelltem MIME-Typ vertrauen
function uploadVulnerable2() {
$allowed = ['image/jpeg', 'image/png', 'image/gif'];
$type = $_FILES['upload']['type']; // Client-kontrolliert!
// Angreifer setzt Content-Type: image/jpeg aber lädt PHP hoch
if (in_array($type, $allowed)) {
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $_FILES['upload']['name']
);
}
}
// VERWUNDBAR: Unvollständige Erweiterungsprüfung
function uploadVulnerable3() {
$filename = $_FILES['upload']['name'];
$ext = pathinfo($filename, PATHINFO_EXTENSION);
// Umgehbar: shell.php.jpg, shell.pHp, shell.php%00.jpg
if ($ext !== 'php') {
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $filename
);
}
}
// VERWUNDBAR: Nur Größe prüfen
function uploadVulnerable4() {
if ($_FILES['upload']['size'] > 0 &&
$_FILES['upload']['size'] < 1000000) {
// Größe zeigt keinen Dateityp an!
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $_FILES['upload']['name']
);
}
}
// VERWUNDBAR: Doppelte Erweiterung Bypass
function uploadVulnerable5() {
$filename = $_FILES['upload']['name'];
// Bekommt nur letzte Erweiterung
if (preg_match('/\.(jpg|png|gif)$/', $filename)) {
// Umgangen: shell.php.jpg (Apache könnte als PHP ausführen)
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $filename
);
}
}
// VERWUNDBAR: Groß-/Kleinschreibung-sensitive Prüfung
function uploadVulnerable6() {
$filename = $_FILES['upload']['name'];
$ext = pathinfo($filename, PATHINFO_EXTENSION);
// Umgangen: shell.PHP, shell.PhP
if ($ext !== 'php' && $ext !== 'phtml') {
move_uploaded_file(
$_FILES['upload']['tmp_name'],
"/uploads/" . $filename
);
}
}
?>
Lösungscode
<?php
// SICHER: Vollständige Datei-Upload-Validierung
class SafeFileUploader {
private const UPLOAD_DIR = '/var/www/uploads/';
private const MAX_SIZE = 5 * 1024 * 1024; // 5MB
// Erlaubte MIME-Typen und ihre gültigen Erweiterungen
private const ALLOWED_TYPES = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'image/gif' => ['gif'],
'application/pdf' => ['pdf']
];
public function upload(array $file): array {
// Auf Upload-Fehler prüfen
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => $this->getErrorMessage($file['error'])];
}
// Dateigröße prüfen
if ($file['size'] > self::MAX_SIZE || $file['size'] === 0) {
return ['success' => false, 'error' => 'Ungültige Dateigröße'];
}
// Verifizieren, dass es eine tatsächlich hochgeladene Datei ist
if (!is_uploaded_file($file['tmp_name'])) {
return ['success' => false, 'error' => 'Kein gültiger Upload'];
}
// MIME-Typ aus Dateiinhalt erkennen (nicht Client-bereitgestellt)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedType = $finfo->file($file['tmp_name']);
// Verifizieren, dass erkannter Typ erlaubt ist
if (!array_key_exists($detectedType, self::ALLOWED_TYPES)) {
return ['success' => false, 'error' => 'Dateityp nicht erlaubt'];
}
// Client-bereitgestellte Erweiterung holen
$clientExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
// Verifizieren, dass Erweiterung zum erkannten Typ passt
if (!in_array($clientExt, self::ALLOWED_TYPES[$detectedType])) {
return ['success' => false, 'error' => 'Erweiterungsfehlanpassung'];
}
// Zusätzliche Inhaltsvalidierung für Bilder
if (strpos($detectedType, 'image/') === 0) {
if (!$this->validateImage($file['tmp_name'])) {
return ['success' => false, 'error' => 'Ungültige Bilddatei'];
}
}
// Sicheren Dateinamen generieren (niemals Client-Dateinamen verwenden)
$safeFilename = $this->generateSafeFilename($clientExt);
$destination = self::UPLOAD_DIR . $safeFilename;
// Datei verschieben
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => false, 'error' => 'Upload fehlgeschlagen'];
}
return [
'success' => true,
'filename' => $safeFilename,
'path' => $destination
];
}
private function generateSafeFilename(string $extension): string {
// Zufälligen Dateinamen generieren, keine Client-Eingabe
return bin2hex(random_bytes(16)) . '.' . $extension;
}
private function validateImage(string $path): bool {
// Verifizieren, dass Bild verarbeitet werden kann
$info = getimagesize($path);
if ($info === false) {
return false;
}
// Auf PHP-Code im Bild prüfen
$content = file_get_contents($path);
if (preg_match('/<\?php|<\?=/i', $content)) {
return false;
}
return true;
}
private function getErrorMessage(int $error): string {
$messages = [
UPLOAD_ERR_INI_SIZE => 'Datei überschreitet upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'Datei überschreitet MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'Datei nur teilweise hochgeladen',
UPLOAD_ERR_NO_FILE => 'Keine Datei hochgeladen',
UPLOAD_ERR_NO_TMP_DIR => 'Temp-Verzeichnis fehlt',
UPLOAD_ERR_CANT_WRITE => 'Schreiben auf Festplatte fehlgeschlagen',
UPLOAD_ERR_EXTENSION => 'Upload durch Extension blockiert'
];
return $messages[$error] ?? 'Unbekannter Fehler';
}
}
// Verwendung
$uploader = new SafeFileUploader();
$result = $uploader->upload($_FILES['upload']);
if ($result['success']) {
echo "Datei hochgeladen: " . $result['filename'];
} else {
echo "Fehler: " . $result['error'];
}
?>
<?php
// SICHER: Alternativer Ansatz mit ImageMagick-Neuverarbeitung
class SafeImageUploader {
public function uploadImage(array $file): ?string {
// Grundlegende Validierung zuerst
if ($file['error'] !== UPLOAD_ERR_OK) {
return null;
}
// Tatsächlichen Typ erkennen
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type = $finfo->file($file['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($type, $allowedTypes)) {
return null;
}
// Bild neu verarbeiten um eingebetteten Code zu entfernen
$image = new Imagick($file['tmp_name']);
$image->stripImage(); // Metadaten entfernen
// Sicheren Dateinamen generieren
$filename = bin2hex(random_bytes(16)) . '.png';
$path = '/var/www/uploads/' . $filename;
// Als PNG speichern (sicheres Format)
$image->setImageFormat('png');
$image->writeImage($path);
$image->destroy();
return $filename;
}
}
// SICHER: Uploads außerhalb des Web-Roots speichern
class SecureUploader {
private const STORAGE_DIR = '/var/storage/uploads/'; // Außerhalb Web-Root
public function upload(array $file): ?string {
// Validierung...
$filename = bin2hex(random_bytes(16));
$path = self::STORAGE_DIR . $filename;
move_uploaded_file($file['tmp_name'], $path);
// Mapping in Datenbank speichern
$this->storeMapping($filename, $file['name'], $file['type']);
return $filename;
}
// Dateien über PHP bereitstellen (kein direkter Zugriff)
public function serve(string $id): void {
$file = $this->getFileInfo($id);
if (!$this->userCanAccess($file)) {
http_response_code(403);
exit;
}
$path = self::STORAGE_DIR . $file['storage_name'];
header('Content-Type: ' . $this->getSafeContentType($path));
header('Content-Disposition: attachment; filename="' .
$this->sanitizeFilename($file['original_name']) . '"');
header('X-Content-Type-Options: nosniff');
readfile($path);
}
}
?>
Ausgenutzt in der Praxis
Web-Shell-Uploads
PHP-Dateien, die als Bilder hochgeladen wurden, führten zu vollständiger Server-Kompromittierung.
Config-Datei-Überschreibungen
Path-Traversal in Dateinamen überschrieb Konfigurationsdateien.
Inhalts-Injection
Bösartige SVG/HTML-Dateien, die Benutzern bereitgestellt wurden, ermöglichten XSS.
Tools zum Testen/Ausnutzen
- Burp Suite — Upload-Anfragen modifizieren
- fuxploider — Datei-Upload-Scanner
- Benutzerdefinierte Skripte für Erweiterungs-/MIME-Manipulation
CVE-Beispiele
- CVEs durch PHP-Datei-Upload-Schwachstellen
- Path-Traversal via Dateinamen in Upload-Handlern
Referenzen
- MITRE. "CWE-616: Incomplete Identification of Uploaded File Variables (PHP)." https://cwe.mitre.org/data/definitions/616.html
- PHP-Dokumentation. File Upload Handling.