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

AuswirkungDetails
IntegritätBereich: Dateiüberschreibung

Path-Traversal kann Systemdateien überschreiben.
VertraulichkeitBereich: Code-Ausführung

Hochladen von PHP-Dateien führt zu Remote-Code-Execution.
SicherheitBereich: 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

  1. MITRE. "CWE-616: Incomplete Identification of Uploaded File Variables (PHP)." https://cwe.mitre.org/data/definitions/616.html
  2. PHP-Dokumentation. File Upload Handling.