Vertrauen auf Dateinamen oder Erweiterung extern bereitgestellter Dateien

Beschreibung

Das Vertrauen auf Dateinamen oder Erweiterung extern bereitgestellter Dateien tritt auf, wenn eine Anwendung Sicherheitsentscheidungen ausschließlich basierend auf dem Dateinamen oder der Erweiterung einer hochgeladenen oder empfangenen Datei trifft, ohne den tatsächlichen Inhalt zu überprüfen. Angreifer können bösartige Dateien (ausführbare Dateien, Skripte, Web-Shells) mit vertrauenswürdigen Erweiterungen oder irreführenden Namen hochladen, um Sicherheitskontrollen zu umgehen.

Risiko

Web-Shells, die mit Bilderweiterungen (.jpg, .gif) hochgeladen, aber als Code ausgeführt werden. Ausführbare Malware, die als Dokumente getarnt ist. Serverseitige Skripte, die Upload-Filter umgehen. Bösartiger Inhalt, der von anfälligen Parsern verarbeitet wird. Ransomware, die über scheinbar harmlose Dateien verbreitet wird. Datenexfiltration durch Dateien, die DLP-Systeme umgehen.

Lösung

Validieren Sie Dateiinhalte mit Magic Bytes und MIME-Erkennung. Verwenden Sie inhaltsbasierte Typprüfung (Dateisignaturen). Benennen Sie hochgeladene Dateien mit generierten Namen um. Speichern Sie Uploads außerhalb des Web-Roots. Implementieren Sie eine Whitelist erlaubter Inhaltstypen, die durch Inspektion verifiziert werden. Verwenden Sie Antivirenscanning. Entfernen Sie Ausführungsberechtigungen von Uploads.

Häufige Konsequenzen

AuswirkungDetails
IntegritätUmfang: Code-Ausführung

Bösartiger Code wird auf Server oder Client ausgeführt.
VertraulichkeitUmfang: Datenleck

Hochgeladene Backdoors ermöglichen dauerhaften Zugriff.
VerfügbarkeitUmfang: Systemkompromittierung

Vollständige Serverübernahme möglich.

Beispielcode + Lösungscode

Anfälliger Code

// ANFÄLLIG: Nur Erweiterungsvalidierung
@RestController
public class VulnerableUploadController {

    private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
        "jpg", "jpeg", "png", "gif", "pdf"
    );

    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) {
        String filename = file.getOriginalFilename();

        // ANFÄLLIG: Prüft nur die Erweiterung
        String extension = getExtension(filename).toLowerCase();
        if (!ALLOWED_EXTENSIONS.contains(extension)) {
            throw new BadRequestException("Dateityp nicht erlaubt");
        }

        // Angreifer lädt shell.php.jpg oder shell.jpg (mit PHP darin) hoch
        // Apache mit Fehlkonfiguration könnte es ausführen!

        Path uploadPath = Paths.get("/uploads", filename);
        Files.copy(file.getInputStream(), uploadPath);

        return "Datei hochgeladen: " + filename;
    }

    // ANFÄLLIG: Doppelte Erweiterung umgeht Prüfung
    @PostMapping("/upload2")
    public String upload2(@RequestParam("file") MultipartFile file) {
        String filename = file.getOriginalFilename();

        // Angreifer: filename = "malware.jpg.exe"
        // Diese Prüfung wird bestanden!
        if (filename.contains(".jpg") || filename.contains(".png")) {
            saveFile(file);
        }
    }
}

// ANFÄLLIG: MIME-Typ aus Request (Client-kontrolliert)
@PostMapping("/upload-mime")
public String uploadMime(@RequestParam("file") MultipartFile file) {
    // ANFÄLLIG: contentType kommt vom Client
    String contentType = file.getContentType();

    if (contentType.startsWith("image/")) {
        // Angreifer setzt Content-Type: image/png für .exe-Datei
        saveFile(file);
    }
}
# ANFÄLLIG: Python nur Erweiterungsvalidierung
import os
from flask import Flask, request

app = Flask(__name__)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}

def allowed_file_vulnerable(filename):
    # ANFÄLLIG: Prüft nur die Erweiterung
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['POST'])
def upload_vulnerable():
    file = request.files['file']

    if not allowed_file_vulnerable(file.filename):
        return 'Dateityp nicht erlaubt', 400

    # Angreifer lädt hoch: shell.php.jpg oder webshell.jpg (mit PHP-Code)
    filename = file.filename
    file.save(os.path.join('/uploads', filename))
    return 'Datei hochgeladen'

# ANFÄLLIG: Null-Byte-Bypass (ältere Systeme)
@app.route('/upload-nullbyte', methods=['POST'])
def upload_nullbyte():
    filename = request.form['filename']

    # Angreifer: filename = "shell.php%00.jpg"
    # Erweiterungsprüfung sieht .jpg
    if filename.endswith('.jpg'):
        # Aber Datei wird auf manchen Systemen als shell.php erstellt
        save_file(filename, request.files['file'])

# ANFÄLLIG: Content-Type aus Request
@app.route('/upload-mime', methods=['POST'])
def upload_mime():
    file = request.files['file']

    # ANFÄLLIG: Vertraut Client-bereitgestelltem MIME-Typ
    if file.content_type.startswith('image/'):
        file.save(os.path.join('/uploads', file.filename))
// ANFÄLLIG: Node.js Erweiterungsvalidierung
const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();

// ANFÄLLIG: Nur Erweiterungsvalidierung
const storage = multer.diskStorage({
    destination: '/uploads',
    filename: (req, file, cb) => {
        cb(null, file.originalname);  // Behält Original-Dateinamen!
    }
});

const fileFilter = (req, file, cb) => {
    const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
    const ext = path.extname(file.originalname).toLowerCase();

    // ANFÄLLIG: Prüft nur die Erweiterung
    if (allowedExtensions.includes(ext)) {
        cb(null, true);
    } else {
        cb(new Error('Ungültiger Dateityp'));
    }
};

const upload = multer({ storage, fileFilter });

app.post('/upload', upload.single('file'), (req, res) => {
    res.json({ filename: req.file.filename });
});

// ANFÄLLIG: MIME-Typ vom Client
const mimeFilter = (req, file, cb) => {
    // file.mimetype kommt vom Client!
    if (file.mimetype.startsWith('image/')) {
        cb(null, true);
    } else {
        cb(new Error('Nur Bilder erlaubt'));
    }
};
<?php
// ANFÄLLIG: Nur Erweiterungsprüfung
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];

$filename = $_FILES['file']['name'];
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

if (!in_array($extension, $allowed)) {
    die('Dateityp nicht erlaubt');
}

// ANFÄLLIG: Original-Dateiname beibehalten
move_uploaded_file($_FILES['file']['tmp_name'], "/uploads/$filename");
// Angreifer lädt shell.php.jpg hoch - kann als PHP ausgeführt werden!

// ANFÄLLIG: MIME-Typ vom Client
$mime = $_FILES['file']['type'];  // Client-kontrolliert!
if (strpos($mime, 'image/') === 0) {
    // Angreifer setzt Content-Type: image/png
    move_uploaded_file($_FILES['file']['tmp_name'], "/uploads/$filename");
}

// ANFÄLLIG: getimagesize() Bypass
if (getimagesize($_FILES['file']['tmp_name'])) {
    // Angreifer können gültigen Bild-Header in bösartige Datei einbetten
    // "Polyglot"-Dateien bestehen diese Prüfung
    move_uploaded_file($_FILES['file']['tmp_name'], "/uploads/$filename");
}
?>

Korrigierter Code

// SICHER: Inhaltsbasierte Validierung
@RestController
public class SafeUploadController {

    private static final Map<String, List<byte[]>> MAGIC_BYTES = Map.of(
        "image/jpeg", List.of(new byte[]{(byte)0xFF, (byte)0xD8, (byte)0xFF}),
        "image/png", List.of(new byte[]{(byte)0x89, 0x50, 0x4E, 0x47}),
        "image/gif", List.of(new byte[]{0x47, 0x49, 0x46, 0x38}),
        "application/pdf", List.of(new byte[]{0x25, 0x50, 0x44, 0x46})
    );

    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) throws IOException {
        // Validiere Inhaltstyp durch Untersuchung des Dateiinhalts
        String detectedType = detectContentType(file.getInputStream());

        if (!MAGIC_BYTES.containsKey(detectedType)) {
            throw new BadRequestException("Dateityp nicht erlaubt");
        }

        // Zusätzliche Validierung mit Apache Tika
        String tikaType = new Tika().detect(file.getInputStream());
        if (!tikaType.startsWith("image/") && !tikaType.equals("application/pdf")) {
            throw new BadRequestException("Ungültiger Dateiinhalt");
        }

        // Generiere sicheren Dateinamen
        String safeFilename = UUID.randomUUID().toString() + getExtensionForType(detectedType);

        // Speichere außerhalb des Web-Roots
        Path uploadPath = Paths.get("/secure-uploads", safeFilename);
        Files.copy(file.getInputStream(), uploadPath);

        return "Datei hochgeladen: " + safeFilename;
    }

    private String detectContentType(InputStream is) throws IOException {
        byte[] header = new byte[8];
        is.read(header);
        is.reset();

        for (Map.Entry<String, List<byte[]>> entry : MAGIC_BYTES.entrySet()) {
            for (byte[] magic : entry.getValue()) {
                if (startsWith(header, magic)) {
                    return entry.getKey();
                }
            }
        }
        return "unknown";
    }

    private boolean startsWith(byte[] data, byte[] prefix) {
        if (data.length < prefix.length) return false;
        for (int i = 0; i < prefix.length; i++) {
            if (data[i] != prefix[i]) return false;
        }
        return true;
    }

    private String getExtensionForType(String type) {
        return switch (type) {
            case "image/jpeg" -> ".jpg";
            case "image/png" -> ".png";
            case "image/gif" -> ".gif";
            case "application/pdf" -> ".pdf";
            default -> "";
        };
    }
}
# SICHER: Inhaltsbasierte Validierung in Python
import magic
import os
import uuid
from PIL import Image
from flask import Flask, request

app = Flask(__name__)

# Erlaubte MIME-Typen durch Inhalt erkannt
ALLOWED_MIME_TYPES = {
    'image/jpeg',
    'image/png',
    'image/gif',
    'application/pdf'
}

# Erweiterungszuordnung für erlaubte Typen
MIME_TO_EXTENSION = {
    'image/jpeg': '.jpg',
    'image/png': '.png',
    'image/gif': '.gif',
    'application/pdf': '.pdf'
}

def validate_file_content(file_stream):
    """Erkenne MIME-Typ aus Dateiinhalt mit libmagic."""
    file_stream.seek(0)
    header = file_stream.read(2048)
    file_stream.seek(0)

    mime = magic.from_buffer(header, mime=True)
    return mime

def validate_image_integrity(file_stream, mime_type):
    """Zusätzliche Validierung für Bilder - prüfe ob sie geöffnet werden können."""
    if not mime_type.startswith('image/'):
        return True

    try:
        file_stream.seek(0)
        img = Image.open(file_stream)
        img.verify()  # Verifiziere dass es ein gültiges Bild ist
        file_stream.seek(0)
        return True
    except Exception:
        return False

@app.route('/upload', methods=['POST'])
def upload_safe():
    file = request.files['file']

    # Erkenne MIME-Typ aus Inhalt
    detected_mime = validate_file_content(file.stream)

    if detected_mime not in ALLOWED_MIME_TYPES:
        return f'Dateityp {detected_mime} nicht erlaubt', 400

    # Zusätzliche Integritätsprüfung für Bilder
    if not validate_image_integrity(file.stream, detected_mime):
        return 'Ungültiges oder beschädigtes Bild', 400

    # Generiere sicheren Dateinamen
    extension = MIME_TO_EXTENSION.get(detected_mime, '')
    safe_filename = str(uuid.uuid4()) + extension

    # Speichere an sicherem Ort (außerhalb Web-Root)
    upload_path = os.path.join('/secure-uploads', safe_filename)
    file.seek(0)
    file.save(upload_path)

    # Setze restriktive Berechtigungen
    os.chmod(upload_path, 0o644)

    return {'filename': safe_filename, 'type': detected_mime}

# SICHER: Mit Antivirenscanning
import clamd

def scan_file(file_path):
    cd = clamd.ClamdUnixSocket()
    result = cd.scan(file_path)
    return result[file_path][0] == 'OK'

@app.route('/upload-scanned', methods=['POST'])
def upload_scanned():
    # ... Validierung wie oben ...

    # Scanne mit Antivirus
    if not scan_file(upload_path):
        os.remove(upload_path)
        return 'Malware erkannt', 400

    return {'filename': safe_filename}
// SICHER: Node.js mit Inhaltsvalidierung
const express = require('express');
const multer = require('multer');
const fileType = require('file-type');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const fs = require('fs');

const app = express();

const ALLOWED_TYPES = new Map([
    ['image/jpeg', '.jpg'],
    ['image/png', '.png'],
    ['image/gif', '.gif'],
    ['application/pdf', '.pdf']
]);

// Speichere temporär zur Validierung
const tempStorage = multer.diskStorage({
    destination: '/tmp/uploads',
    filename: (req, file, cb) => {
        cb(null, uuidv4());
    }
});

const upload = multer({
    storage: tempStorage,
    limits: { fileSize: 10 * 1024 * 1024 }  // 10MB Limit
});

app.post('/upload', upload.single('file'), async (req, res) => {
    const tempPath = req.file.path;

    try {
        // Erkenne Typ aus Dateiinhalt
        const type = await fileType.fromFile(tempPath);

        if (!type || !ALLOWED_TYPES.has(type.mime)) {
            fs.unlinkSync(tempPath);
            return res.status(400).json({
                error: `Dateityp ${type?.mime || 'unbekannt'} nicht erlaubt`
            });
        }

        // Für Bilder zusätzliche Validierung
        if (type.mime.startsWith('image/')) {
            const sharp = require('sharp');
            try {
                await sharp(tempPath).metadata();
            } catch (e) {
                fs.unlinkSync(tempPath);
                return res.status(400).json({ error: 'Ungültige Bilddatei' });
            }
        }

        // Verschiebe zu endgültigem Speicherort mit sicherem Namen
        const extension = ALLOWED_TYPES.get(type.mime);
        const safeFilename = uuidv4() + extension;
        const finalPath = path.join('/secure-uploads', safeFilename);

        fs.renameSync(tempPath, finalPath);
        fs.chmodSync(finalPath, 0o644);

        res.json({ filename: safeFilename, type: type.mime });

    } catch (error) {
        if (fs.existsSync(tempPath)) {
            fs.unlinkSync(tempPath);
        }
        res.status(500).json({ error: 'Upload fehlgeschlagen' });
    }
});
<?php
// SICHER: Inhaltsbasierte Validierung in PHP
class SafeFileUpload {
    private $allowedTypes = [
        'image/jpeg' => '.jpg',
        'image/png' => '.png',
        'image/gif' => '.gif',
        'application/pdf' => '.pdf'
    ];

    private $uploadDir = '/secure-uploads/';

    public function upload($file) {
        // Erkenne MIME aus Inhalt mit finfo
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $detectedMime = $finfo->file($file['tmp_name']);

        if (!array_key_exists($detectedMime, $this->allowedTypes)) {
            throw new Exception("Dateityp $detectedMime nicht erlaubt");
        }

        // Zusätzliche Validierung für Bilder
        if (strpos($detectedMime, 'image/') === 0) {
            if (!$this->validateImage($file['tmp_name'])) {
                throw new Exception("Ungültiges oder beschädigtes Bild");
            }
        }

        // Generiere sicheren Dateinamen
        $extension = $this->allowedTypes[$detectedMime];
        $safeFilename = bin2hex(random_bytes(16)) . $extension;
        $finalPath = $this->uploadDir . $safeFilename;

        // Verschiebe Datei
        if (!move_uploaded_file($file['tmp_name'], $finalPath)) {
            throw new Exception("Upload fehlgeschlagen");
        }

        // Setze restriktive Berechtigungen
        chmod($finalPath, 0644);

        return [
            'filename' => $safeFilename,
            'type' => $detectedMime
        ];
    }

    private function validateImage($path) {
        // Verwende GD zur Überprüfung der Bildintegrität
        $imageInfo = @getimagesize($path);
        if (!$imageInfo) {
            return false;
        }

        // Versuche das Bild tatsächlich zu laden
        switch ($imageInfo[2]) {
            case IMAGETYPE_JPEG:
                $img = @imagecreatefromjpeg($path);
                break;
            case IMAGETYPE_PNG:
                $img = @imagecreatefrompng($path);
                break;
            case IMAGETYPE_GIF:
                $img = @imagecreatefromgif($path);
                break;
            default:
                return false;
        }

        if (!$img) {
            return false;
        }

        imagedestroy($img);
        return true;
    }
}

// Verwendung
$uploader = new SafeFileUpload();
try {
    $result = $uploader->upload($_FILES['file']);
    echo json_encode($result);
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode(['error' => $e->getMessage()]);
}
?>

Ausgenutzt in der Praxis

Web-Shell-Uploads

PHP/ASP-Shells mit Bilderweiterungen werden auf Servern ausgeführt.

Malware-Verbreitung

Ausführbare Dateien, die als Dokumente getarnt sind.

RCE über Uploads

Serverkompromittierung durch bösartige Datei-Uploads.


Tools zum Testen/Ausnutzen

  • Burp Suite - Upload-Tests.

  • Polyglot-Dateigeneratoren.

  • Schwachstellenscanner für Datei-Uploads.


CVE-Beispiele

  • CVE-2020-13942: Apache Unomi RCE über Datei-Upload.

  • Zahlreiche CVEs für Web-Shell-Uploads.


Referenzen

  1. MITRE. "CWE-646: Reliance on File Name or Extension of Externally-Supplied File." https://cwe.mitre.org/data/definitions/646.html

  2. OWASP. "Unrestricted File Upload."