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
| Impact | Details |
|---|---|
| Integrity | Scope: File System Unauthorized file creation or overwrite. |
| Availability | Scope: Denial of Service Reserved names cause system issues. |
| Confidentiality | Scope: 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
-
MITRE. "CWE-641: Improper Restriction of Names for Files and Other Resources." https://cwe.mitre.org/data/definitions/641.html
-
OWASP. "Path Traversal."