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
| Auswirkung | Details |
|---|---|
| Integrität | Umfang: Code-Ausführung Bösartiger Code wird auf Server oder Client ausgeführt. |
| Vertraulichkeit | Umfang: Datenleck Hochgeladene Backdoors ermöglichen dauerhaften Zugriff. |
| Verfügbarkeit | Umfang: 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
-
MITRE. "CWE-646: Reliance on File Name or Extension of Externally-Supplied File." https://cwe.mitre.org/data/definitions/646.html
-
OWASP. "Unrestricted File Upload."