Unzureichende Einschränkung von Namen für Dateien und andere Ressourcen

Beschreibung

Unzureichende Einschränkung von Namen für Dateien und andere Ressourcen tritt auf, wenn eine Anwendung Dateien, Verzeichnisse, Datenbanktabellen oder andere Ressourcen mit Namen erstellt, die von Benutzereingaben abgeleitet sind, ohne ordnungsgemäße Validierung. Angreifer können Namen mit Sonderzeichen, Pfadtrennern oder reservierten Namen erstellen, um unbeabsichtigtes Verhalten zu verursachen, einschließlich Path-Traversal, Ressourcen-Hijacking, Denial of Service oder Code-Ausführung.

Risiko

Path-Traversal durch Namen mit "../"-Sequenzen. Überschreiben kritischer Systemdateien. Kollisionsangriffe durch Erstellen von Ressourcen mit Namen, die mit bestehenden kollidieren. Denial of Service durch reservierte Namen (CON, NUL unter Windows). Erstellen ausführbarer Dateien in Web-Verzeichnissen. SQL-Injection durch Tabellen-/Spaltennamen. Shell-Befehlsinjektion durch Dateinamen.

Lösung

Validieren Sie Ressourcennamen gegen eine strikte Whitelist erlaubter Zeichen. Lehnen Sie Namen mit Pfadtrennern, Null-Bytes und Steuerzeichen ab. Bereinigen oder lehnen Sie reservierte Namen ab. Begrenzen Sie die Namenslänge. Verwenden Sie generierte Namen anstelle von benutzerbereitgestellten. Implementieren Sie ordnungsgemäße Zugriffskontrollen unabhängig von der Benennung.

Häufige Auswirkungen

AuswirkungDetails
IntegritätBereich: Dateisystem

Unbefugte Dateierstellung oder -überschreibung.
VerfügbarkeitBereich: Denial of Service

Reservierte Namen verursachen Systemprobleme.
VertraulichkeitBereich: Informationsoffenlegung

Zugriff auf Dateien außerhalb des beabsichtigten Verzeichnisses.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: Benutzergesteuerter Dateiname
public class VulnerableFileUpload {

    public void saveUpload(String userFilename, byte[] content) {
        // Keine Validierung des Dateinamens
        File file = new File("/uploads/" + userFilename);
        Files.write(file.toPath(), content);
    }
    // Angriff: userFilename = "../../../etc/cron.d/malicious"
    // Angriff: userFilename = "../../webapps/ROOT/shell.jsp"
}

// VERWUNDBAR: Datenbanktabellen aus Benutzereingabe erstellen
public class VulnerableDatabaseCreator {

    public void createUserTable(String username) {
        String tableName = "user_" + username;
        // SQL-Injection durch Tabellennamen!
        String sql = "CREATE TABLE " + tableName + " (id INT, data TEXT)";
        connection.executeUpdate(sql);
    }
    // Angriff: username = "test; DROP TABLE users; --"
}

// VERWUNDBAR: Verzeichniserstellung
public class VulnerableDirectoryService {

    public void createProjectDir(String projectName) {
        // Keine Validierung
        File dir = new File("/projects/" + projectName);
        dir.mkdirs();
    }
    // Angriff: projectName = "../../sensitive/admin"
}
# VERWUNDBAR: Python-Dateioperationen mit Benutzereingabe
import os

def save_file_vulnerable(username, filename, content):
    # Keine Validierung
    path = f"/data/{username}/{filename}"
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, 'w') as f:
        f.write(content)
    # Angriff: filename = "../../../etc/passwd"

# VERWUNDBAR: Datenbank mit benutzergesteuerten Namen
def create_user_schema_vulnerable(username):
    schema_name = f"user_{username}"
    # SQL-Injection durch Schemanamen
    cursor.execute(f"CREATE SCHEMA {schema_name}")
    # Angriff: username = "test; DROP DATABASE production;--"

# VERWUNDBAR: Python-Module dynamisch erstellen
def create_plugin_vulnerable(plugin_name, code):
    # Schreibt in plugins-Verzeichnis
    filename = f"/app/plugins/{plugin_name}.py"
    with open(filename, 'w') as f:
        f.write(code)
    # Angriff: plugin_name = "__init__"
    # Angriff: plugin_name = "../../../app/main"

# VERWUNDBAR: Session-Datei-Speicherung
def save_session_vulnerable(session_id, data):
    # Session-ID als Dateiname verwendet
    path = f"/tmp/sessions/{session_id}"
    with open(path, 'w') as f:
        f.write(data)
    # Angriff: session_id = "../../root/.ssh/authorized_keys"
// VERWUNDBAR: Node.js Dateioperationen
const fs = require('fs');
const path = require('path');

function saveUserFileVulnerable(userId, filename, content) {
    // Keine Validierung
    const filePath = `/uploads/${userId}/${filename}`;
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, content);
}
// Angriff: filename = "../../../app/routes/backdoor.js"

// VERWUNDBAR: Temporäre Dateien erstellen
function createTempFileVulnerable(name, data) {
    const tempPath = `/tmp/${name}`;
    fs.writeFileSync(tempPath, data);
}
// Angriff: name = "../../etc/ld.so.preload"

// VERWUNDBAR: Datenbank-Collection-Namen (MongoDB)
async function createCollectionVulnerable(collectionName) {
    // Benutzer kontrolliert Collection-Namen
    await db.createCollection(collectionName);
}
// Angriff: collectionName = "admin.system.users"

// VERWUNDBAR: Log-Datei-Benennung
function createLogFileVulnerable(logName) {
    const logPath = `/logs/${logName}.log`;
    return fs.createWriteStream(logPath);
}
// Angriff: logName = "../../../var/www/shell.php\x00"

Lösungscode

// SICHER: Validierte Dateinamen-Behandlung
public class SafeFileUpload {

    private static final Pattern SAFE_FILENAME = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$");
    private static final Set<String> RESERVED_NAMES = Set.of(
        "con", "prn", "aux", "nul",
        "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9",
        "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9"
    );

    public void saveUpload(String userFilename, byte[] content) {
        String safeName = sanitizeFilename(userFilename);

        // Festes Basisverzeichnis verwenden
        Path basePath = Paths.get("/uploads").toAbsolutePath().normalize();
        Path filePath = basePath.resolve(safeName).normalize();

        // Verifizieren, dass Pfad innerhalb Basisverzeichnis ist
        if (!filePath.startsWith(basePath)) {
            throw new SecurityException("Ungültiger Pfad");
        }

        Files.write(filePath, content);
    }

    private String sanitizeFilename(String filename) {
        if (filename == null || filename.isEmpty()) {
            throw new IllegalArgumentException("Dateiname erforderlich");
        }

        // Pfadkomponenten entfernen
        String name = Paths.get(filename).getFileName().toString();

        // Null-Bytes und Steuerzeichen entfernen
        name = name.replaceAll("[\\x00-\\x1f\\x7f]", "");

        // Gegen sicheres Muster validieren
        if (!SAFE_FILENAME.matcher(name).matches()) {
            throw new IllegalArgumentException("Ungültiger Dateiname");
        }

        // Reservierte Namen prüfen
        String baseName = name.contains(".") ?
            name.substring(0, name.lastIndexOf('.')) : name;
        if (RESERVED_NAMES.contains(baseName.toLowerCase())) {
            throw new IllegalArgumentException("Reservierter Dateiname");
        }

        return name;
    }
}

// SICHER: Datenbank mit parametrisierten Identifikatoren
public class SafeDatabaseCreator {

    private static final Pattern SAFE_IDENTIFIER = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{0,63}$");

    public void createUserTable(String username) {
        // Identifikator validieren
        String safeUsername = validateIdentifier(username);
        String tableName = "user_" + safeUsername;

        // Identifikator-Quoting verwenden
        String sql = "CREATE TABLE " + quoteIdentifier(tableName) +
                     " (id INT, data TEXT)";
        connection.executeUpdate(sql);
    }

    private String validateIdentifier(String name) {
        if (!SAFE_IDENTIFIER.matcher(name).matches()) {
            throw new IllegalArgumentException("Ungültiger Identifikator");
        }
        return name;
    }

    private String quoteIdentifier(String identifier) {
        // Datenbankspezifisches Quoting
        return "\"" + identifier.replace("\"", "\"\"") + "\"";
    }
}
import re
import os
import uuid
from pathlib import Path

# SICHER: Validierte Dateioperationen
class SafeFileHandler:
    SAFE_NAME_PATTERN = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}

```javascript
// SICHER: Node.js mit ordnungsgemäßer Validierung
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');

class SafeFileHandler {
    constructor(baseDir) {
        this.baseDir = path.resolve(baseDir);
    }

    static SAFE_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$/;
    static RESERVED_NAMES = new Set([
        'con', 'prn', 'aux', 'nul',
        'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
        'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'
    ]);

    sanitizeFilename(filename) {
        if (!filename) {
            throw new Error('Dateiname erforderlich');
        }

        // Nur Dateinamen extrahieren
        const name = path.basename(filename);

        // Null-Bytes und Steuerzeichen entfernen
        const cleaned = name.replace(/[\x00-\x1f\x7f]/g, '');

        // Muster validieren
        if (!SafeFileHandler.SAFE_NAME_REGEX.test(cleaned)) {
            throw new Error('Ungültiges Dateinamenformat');
        }

        // Reservierte Namen prüfen
        const baseName = cleaned.includes('.') ?
            cleaned.substring(0, cleaned.lastIndexOf('.')) : cleaned;

        if (SafeFileHandler.RESERVED_NAMES.has(baseName.toLowerCase())) {
            throw new Error('Reservierter Dateiname');
        }

        return cleaned;
    }

    saveFile(filename, content) {
        const safeName = this.sanitizeFilename(filename);
        const filePath = path.resolve(this.baseDir, safeName);

        // Innerhalb Basisverzeichnis verifizieren
        if (!filePath.startsWith(this.baseDir + path.sep) &&
            filePath !== this.baseDir) {
            throw new Error('Path-Traversal erkannt');
        }

        fs.writeFileSync(filePath, content);
        return safeName;
    }

    saveWithGeneratedName(extension, content) {
        // Erweiterung validieren
        if (extension && !/^\.[a-zA-Z0-9]+$/.test(extension)) {
            throw new Error('Ungültige Erweiterung');
        }

        const filename = uuidv4() + (extension || '');
        return this.saveFile(filename, content);
    }
}

// SICHER: MongoDB Collection-Namen
const SAFE_COLLECTION_NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
const RESERVED_COLLECTIONS = ['admin', 'local', 'config', 'system'];

function createCollectionSafe(name) {
    if (!SAFE_COLLECTION_NAME.test(name)) {
        throw new Error('Ungültiger Collection-Name');
    }

    if (RESERVED_COLLECTIONS.some(r => name.startsWith(r + '.'))) {
        throw new Error('Reservierter Collection-Name');
    }

    return db.createCollection(name);
}

Ausgenutzt in der Praxis

Path-Traversal-Uploads

Bösartige Dateinamen verwendet, um außerhalb des Upload-Verzeichnisses zu schreiben.

Windows-DoS

Reservierte Gerätenamen verursachen Anwendungs-Hänger.

SQL-Injection

Tabellen-/Spaltennamen ermöglichen Injection-Angriffe.


Tools zum Testen/Ausnutzen

  • Burp Suite — Dateinamen-Fuzzing.
  • Path-Traversal-Wortlisten.
  • Reservierte-Namen-Tests.

CVE-Beispiele

  • CVE-2019-12415: Apache POI Path-Traversal über Dateinamen.
  • Zahlreiche Zip-Slip-Schwachstellen.

Referenzen

  1. MITRE. "CWE-641: Improper Restriction of Names for Files and Other Resources." https://cwe.mitre.org/data/definitions/641.html
  2. OWASP. "Path Traversal."