Improper Restriction of Names for Files and Other Resources

Description

Improper Restriction of Names for Files and Other Resources occurs when an application creates files, directories, database tables, or other resources using names derived from user input without proper validation. Attackers can craft names containing special characters, path separators, or reserved names to cause unintended behavior including path traversal, resource hijacking, denial of service, or code execution.

Risk

Path traversal through names containing "../" sequences. Overwriting critical system files. Collision attacks by creating resources with names that conflict with existing ones. Denial of service through reserved names (CON, NUL on Windows). Creating executable files in web directories. SQL injection through table/column names. Shell command injection through filenames.

Solution

Validate resource names against a strict whitelist of allowed characters. Reject names with path separators, null bytes, and control characters. Sanitize or reject reserved names. Limit name length. Use generated names instead of user-provided ones. Implement proper access controls independent of naming.

Common Consequences

ImpactDetails
IntegrityScope: File System

Unauthorized file creation or overwrite.
AvailabilityScope: Denial of Service

Reserved names cause system issues.
ConfidentialityScope: Information Disclosure

Access to files outside intended directory.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: User-controlled filename
public class VulnerableFileUpload {

    public void saveUpload(String userFilename, byte[] content) {
        // No validation of filename
        File file = new File("/uploads/" + userFilename);
        Files.write(file.toPath(), content);
    }
    // Attack: userFilename = "../../../etc/cron.d/malicious"
    // Attack: userFilename = "../../webapps/ROOT/shell.jsp"
}

// VULNERABLE: Creating database tables from user input
public class VulnerableDatabaseCreator {

    public void createUserTable(String username) {
        String tableName = "user_" + username;
        // SQL injection through table name!
        String sql = "CREATE TABLE " + tableName + " (id INT, data TEXT)";
        connection.executeUpdate(sql);
    }
    // Attack: username = "test; DROP TABLE users; --"
}

// VULNERABLE: Directory creation
public class VulnerableDirectoryService {

    public void createProjectDir(String projectName) {
        // No validation
        File dir = new File("/projects/" + projectName);
        dir.mkdirs();
    }
    // Attack: projectName = "../../sensitive/admin"
}
# VULNERABLE: Python file operations with user input
import os

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

# VULNERABLE: Database with user-controlled names
def create_user_schema_vulnerable(username):
    schema_name = f"user_{username}"
    # SQL injection through schema name
    cursor.execute(f"CREATE SCHEMA {schema_name}")
    # Attack: username = "test; DROP DATABASE production;--"

# VULNERABLE: Creating Python modules dynamically
def create_plugin_vulnerable(plugin_name, code):
    # Writes to plugins directory
    filename = f"/app/plugins/{plugin_name}.py"
    with open(filename, 'w') as f:
        f.write(code)
    # Attack: plugin_name = "__init__"
    # Attack: plugin_name = "../../../app/main"

# VULNERABLE: Session file storage
def save_session_vulnerable(session_id, data):
    # Session ID used as filename
    path = f"/tmp/sessions/{session_id}"
    with open(path, 'w') as f:
        f.write(data)
    # Attack: session_id = "../../root/.ssh/authorized_keys"
// VULNERABLE: Node.js file operations
const fs = require('fs');
const path = require('path');

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

// VULNERABLE: Creating temporary files
function createTempFileVulnerable(name, data) {
    const tempPath = `/tmp/${name}`;
    fs.writeFileSync(tempPath, data);
}
// Attack: name = "../../etc/ld.so.preload"

// VULNERABLE: Database collection names (MongoDB)
async function createCollectionVulnerable(collectionName) {
    // User controls collection name
    await db.createCollection(collectionName);
}
// Attack: collectionName = "admin.system.users"

// VULNERABLE: Log file naming
function createLogFileVulnerable(logName) {
    const logPath = `/logs/${logName}.log`;
    return fs.createWriteStream(logPath);
}
// Attack: logName = "../../../var/www/shell.php\x00"
<?php
// VULNERABLE: File upload handling
function uploadFileVulnerable($userFilename, $tmpPath) {
    $destination = "/var/www/uploads/" . $userFilename;
    move_uploaded_file($tmpPath, $destination);
}
// Attack: userFilename = "../public/shell.php"

// VULNERABLE: Cache file creation
function createCacheVulnerable($key, $data) {
    $cacheFile = "/cache/" . $key . ".cache";
    file_put_contents($cacheFile, serialize($data));
}
// Attack: key = "../../../../var/www/config"

// VULNERABLE: User directory creation
function createUserDirectoryVulnerable($username) {
    $path = "/home/users/" . $username;
    mkdir($path, 0755, true);
}
// Attack: username = "../../../etc/new_dir"

// VULNERABLE: Windows reserved names
function createFileWindowsVulnerable($name) {
    // On Windows, these cause issues
    $path = "C:\\data\\" . $name;
    file_put_contents($path, "data");
}
// Attack: name = "CON" or "PRN" or "NUL" - causes hang/error
?>

Fixed Code

// SAFE: Validated filename handling
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);

        // Use fixed base directory
        Path basePath = Paths.get("/uploads").toAbsolutePath().normalize();
        Path filePath = basePath.resolve(safeName).normalize();

        // Verify path is within base directory
        if (!filePath.startsWith(basePath)) {
            throw new SecurityException("Invalid path");
        }

        Files.write(filePath, content);
    }

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

        // Remove path components
        String name = Paths.get(filename).getFileName().toString();

        // Remove null bytes and control characters
        name = name.replaceAll("[\\x00-\\x1f\\x7f]", "");

        // Validate against safe pattern
        if (!SAFE_FILENAME.matcher(name).matches()) {
            throw new IllegalArgumentException("Invalid filename");
        }

        // Check reserved names
        String baseName = name.contains(".") ?
            name.substring(0, name.lastIndexOf('.')) : name;
        if (RESERVED_NAMES.contains(baseName.toLowerCase())) {
            throw new IllegalArgumentException("Reserved filename");
        }

        return name;
    }
}

// SAFE: Database with parameterized identifiers
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) {
        // Validate identifier
        String safeUsername = validateIdentifier(username);
        String tableName = "user_" + safeUsername;

        // Use identifier quoting
        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("Invalid identifier");
        }
        return name;
    }

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

# SAFE: Validated file operations
class SafeFileHandler:
    SAFE_NAME_PATTERN = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$')
    RESERVED_NAMES = {
        'con', 'prn', 'aux', 'nul',
        'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
        'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'
    }

    def __init__(self, base_dir: str):
        self.base_dir = Path(base_dir).resolve()

    def sanitize_filename(self, filename: str) -> str:
        if not filename:
            raise ValueError("Filename required")

        # Extract just the filename
        name = Path(filename).name

        # Remove null bytes and control chars
        name = re.sub(r'[\x00-\x1f\x7f]', '', name)

        # Validate pattern
        if not self.SAFE_NAME_PATTERN.match(name):
            raise ValueError("Invalid filename format")

        # Check reserved names
        base_name = name.rsplit('.', 1)[0] if '.' in name else name
        if base_name.lower() in self.RESERVED_NAMES:
            raise ValueError("Reserved filename")

        return name

    def save_file(self, filename: str, content: bytes) -> Path:
        safe_name = self.sanitize_filename(filename)
        file_path = (self.base_dir / safe_name).resolve()

        # Verify within base directory
        if not str(file_path).startswith(str(self.base_dir)):
            raise ValueError("Path traversal detected")

        file_path.write_bytes(content)
        return file_path

    def save_with_generated_name(self, extension: str, content: bytes) -> str:
        """Use generated names for maximum safety."""
        if extension and not extension.startswith('.'):
            extension = '.' + extension

        # Validate extension
        if extension and not re.match(r'^\.[a-zA-Z0-9]+$', extension):
            raise ValueError("Invalid extension")

        # Generate random name
        filename = str(uuid.uuid4()) + (extension or '')
        self.save_file(filename, content)
        return filename

# SAFE: Database identifiers
class SafeDatabaseHelper:
    SAFE_IDENTIFIER = re.compile(r'^[a-zA-Z][a-zA-Z0-9_]{0,63}$')

    def validate_identifier(self, name: str) -> str:
        if not self.SAFE_IDENTIFIER.match(name):
            raise ValueError(f"Invalid identifier: {name}")
        return name

    def create_user_schema(self, username: str):
        safe_name = self.validate_identifier(username)
        # Use parameterized query where possible, or proper quoting
        schema_name = f"user_{safe_name}"

        # Proper identifier quoting
        quoted = self.quote_identifier(schema_name)
        cursor.execute(f"CREATE SCHEMA {quoted}")

    def quote_identifier(self, name: str) -> str:
        # PostgreSQL style
        return '"' + name.replace('"', '""') + '"'
// SAFE: Node.js with proper validation
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('Filename required');
        }

        // Extract just filename
        const name = path.basename(filename);

        // Remove null bytes and control characters
        const cleaned = name.replace(/[\x00-\x1f\x7f]/g, '');

        // Validate pattern
        if (!SafeFileHandler.SAFE_NAME_REGEX.test(cleaned)) {
            throw new Error('Invalid filename format');
        }

        // Check reserved names
        const baseName = cleaned.includes('.') ?
            cleaned.substring(0, cleaned.lastIndexOf('.')) : cleaned;

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

        return cleaned;
    }

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

        // Verify within base directory
        if (!filePath.startsWith(this.baseDir + path.sep) &&
            filePath !== this.baseDir) {
            throw new Error('Path traversal detected');
        }

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

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

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

// SAFE: MongoDB collection names
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('Invalid collection name');
    }

    if (RESERVED_COLLECTIONS.some(r => name.startsWith(r + '.'))) {
        throw new Error('Reserved collection name');
    }

    return db.createCollection(name);
}

Exploited in the Wild

Path Traversal Uploads

Malicious filenames used to write outside upload directory.

Windows DoS

Reserved device names causing application hangs.

SQL Injection

Table/column names enabling injection attacks.


Tools to test/exploit

  • Burp Suite — filename fuzzing.

  • Path traversal wordlists.

  • Reserved name testing.


CVE Examples

  • CVE-2019-12415: Apache POI path traversal via filenames.

  • Numerous zip slip vulnerabilities.


References

  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."