JavaScript Non-Literal Filesystem Filename

High Risk Path Traversal
javascriptpath-traversalfilesystemfile-accessdirectory-traversal

What it is

The JavaScript application uses dynamic or user-controlled input to construct filesystem paths for file operations, potentially leading to path traversal attacks. This vulnerability allows attackers to access files outside the intended directory structure, potentially reading sensitive files or overwriting critical system files.

// Vulnerable: User input directly in file path
const fs = require('fs');
const path = require('path');

app.get('/download/:filename', (req, res) => {
  const filename = req.params.filename;
  
  // Dangerous: User-controlled filename
  const filePath = path.join(__dirname, 'uploads', filename);
  
  fs.readFile(filePath, (err, data) => {
    if (err) {
      return res.status(404).send('File not found');
    }
    res.send(data);
  });
});
// Secure: Path validation and sanitization
const fs = require('fs');
const path = require('path');

const UPLOAD_DIR = path.resolve(__dirname, 'uploads');
const ALLOWED_EXTENSIONS = ['.txt', '.pdf', '.jpg', '.png'];

app.get('/download/:filename', (req, res) => {
  const filename = req.params.filename;
  
  // Validate filename format
  if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
    return res.status(400).json({ error: 'Invalid filename' });
  }
  
  // Check file extension
  const ext = path.extname(filename).toLowerCase();
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
    return res.status(400).json({ error: 'File type not allowed' });
  }
  
  // Resolve and validate path
  const filePath = path.resolve(UPLOAD_DIR, filename);
  
  // Ensure file is within upload directory
  if (!filePath.startsWith(UPLOAD_DIR)) {
    return res.status(400).json({ error: 'Invalid file path' });
  }
  
  fs.readFile(filePath, (err, data) => {
    if (err) {
      return res.status(404).json({ error: 'File not found' });
    }
    res.send(data);
  });
});

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Node.js applications construct file paths using user-controlled input without validation: fs.readFile(req.query.filename, callback) or fs.writeFile('/uploads/' + req.body.name, data). Attackers provide path traversal sequences like '../../../etc/passwd' to access files outside intended directory. User input from req.query, req.params, req.body, or file upload names gets directly concatenated into file paths. Applications assume users provide only legitimate filenames without considering malicious input. Path traversal allows reading sensitive files (configuration, credentials, source code), overwriting critical files, or accessing other users' data in multi-tenant applications. Even seemingly safe operations like reading uploaded files become vulnerable when filenames aren't validated.

Root causes

Using User Input Directly in File Paths Without Validation

Node.js applications construct file paths using user-controlled input without validation: fs.readFile(req.query.filename, callback) or fs.writeFile('/uploads/' + req.body.name, data). Attackers provide path traversal sequences like '../../../etc/passwd' to access files outside intended directory. User input from req.query, req.params, req.body, or file upload names gets directly concatenated into file paths. Applications assume users provide only legitimate filenames without considering malicious input. Path traversal allows reading sensitive files (configuration, credentials, source code), overwriting critical files, or accessing other users' data in multi-tenant applications. Even seemingly safe operations like reading uploaded files become vulnerable when filenames aren't validated.

Dynamic Filename Construction with Untrusted Data

Applications dynamically build file paths from multiple untrusted sources: fs.readFile(`./data/${userFolder}/${userFile}`) combining user-controlled variables. Template literals make string interpolation convenient encouraging direct embedding of untrusted data in paths. Code uses database values for file paths without recognizing data may contain previously injected malicious paths. URL slugs, session data, or JWT claims used to construct file paths assuming these sources are trusted. Complex path construction logic makes it difficult to identify where user input enters path, hiding security issues during code review. Developers focus on business logic (serving user's files) without considering security implications of dynamic path construction.

Missing Path Sanitization Before Filesystem Operations

File operations execute without sanitizing or normalizing paths to detect traversal attempts. Applications don't remove or reject ../ sequences, absolute paths, or null bytes before file operations. No use of path normalization functions (path.normalize(), path.resolve()) to canonicalize paths before validation. String-based sanitization like filename.replace('..', '') gets bypassed with ..././ or URL-encoded variants (%2e%2e%2f). Applications check for ../ in filename but miss absolute paths (/etc/passwd), Windows paths (C:\), or symlink exploitation. Sanitization applied inconsistently - some routes validate paths while others skip checks. Filesystem operations happen in utility functions where path validation is assumed to occur at caller, but doesn't.

Insufficient Validation of File Upload Paths

File upload handling uses user-provided filenames without validation: multer stores files at req.file.originalname location, busboy writes to user-specified paths. Applications preserve original upload filenames for convenience allowing attackers to upload files with malicious names like ../../../../tmp/backdoor.js. Upload middleware configured with dynamic destination paths based on user input: multer({dest: './uploads/' + req.body.userId}). No validation that uploaded files stay within designated upload directory. Applications check file content-type and size but skip filename validation. Path traversal in upload filenames allows overwriting application files, writing to web-accessible directories for code execution, or filling filesystem causing DoS.

Using path.join() with Unvalidated User Input

Applications use Node.js path.join() believing it provides security against path traversal: path.join('/safe/directory', userInput). However, path.join() doesn't prevent traversal - it normalizes paths but allows ../ to traverse up directory tree. Absolute paths passed to path.join() replace the base path entirely: path.join('/safe', '/etc/passwd') returns /etc/passwd. Developers misunderstand path.join() security model thinking it restricts access to base directory. Code uses path.join() without subsequent validation that result stays within intended directory. Even with path.join(), need to use path.resolve() to get absolute path and verify it starts with intended base directory using startsWith() check.

Fixes

1

Validate and Sanitize All File Paths Before Use

Implement comprehensive path validation before any filesystem operations. Use path.normalize() to canonicalize paths removing redundant separators and resolving . and .. sequences: const normalized = path.normalize(userPath). Then use path.resolve() to get absolute path: const absolute = path.resolve(baseDir, normalized). Validate resulting path starts with intended base directory: if (!absolute.startsWith(path.resolve(baseDir))) throw new Error('Path traversal detected'). Reject paths containing null bytes (\0), which can truncate paths in C libraries. Check for symlinks using fs.lstat() to prevent symlink-based traversal. Validate filename length and character set - reject paths with unusual characters. Apply validation consistently across all file operations using shared utility functions ensuring no code path bypasses security checks.

2

Implement Strict Filename and Directory Allowlists

Create explicit allowlists of permitted filenames, directories, or filename patterns rather than trying to blocklist dangerous patterns. Define allowed filename characters: const SAFE_FILENAME_REGEX = /^[a-zA-Z0-9_\-\.]+$/; if (!SAFE_FILENAME_REGEX.test(filename)) reject. For directory access, maintain allowlist of permitted subdirectories: const ALLOWED_DIRS = ['uploads', 'public', 'temp']; validate directory is in list. Use indirect references where users select from predefined options (file IDs mapped to actual paths server-side) rather than providing paths directly. Implement filename extension allowlist for uploads: const ALLOWED_EXTENSIONS = ['.jpg', '.png', '.pdf']; validate extension matches. Never attempt to sanitize filenames - reject invalid input and require users to provide valid filenames.

3

Use path.resolve() and Validate Against Base Directory

Always use path.resolve() to convert relative paths to absolute paths before validation: const safePath = path.resolve(baseDirectory, userProvidedPath). Then verify resolved path is within intended base directory: const baseDir = path.resolve('./uploads'); if (!safePath.startsWith(baseDir + path.sep)) throw new Error('Access denied'). Use path.sep (OS-specific separator) rather than hardcoded / to work correctly on Windows. The startsWith check must include separator to prevent prefix attacks: /uploads and /uploads-evil both start with /uploads but second is invalid. For more robust validation, use path.relative() to get relative path from base to target and check it doesn't start with ..: if (path.relative(baseDir, safePath).startsWith('..')) reject. Test validation with various traversal attempts: ../, ../../, absolute paths, Windows paths.

4

Use Indirect Object References Instead of Direct Paths

Replace direct file path handling with indirect object reference pattern where users reference files by opaque IDs rather than paths. Store mapping of file IDs to server-side paths in database: fileMap[randomId] = actualPath. When users request files, look up actual path: const filePath = await db.getFilePathById(fileId); fs.readFile(filePath, ...). Generate file IDs using cryptographically secure random values: crypto.randomBytes(16).toString('hex'). Implement access control in database layer: SELECT path FROM files WHERE id = ? AND owner_id = ?. This architectural change eliminates path traversal attack surface entirely as users never provide paths. For file uploads, generate server-controlled filenames: const filename = crypto.randomUUID() + path.extname(originalName); store in database with original name for display only.

5

Restrict File Operations to Specific Safe Directories

Configure application to perform all file operations within strictly defined safe directories isolated from sensitive system files and application code. Use chroot jail, Docker containers with volume mounts, or dedicated file storage services (AWS S3, Azure Blob Storage) to isolate file storage from application. Set working directory to safe location: process.chdir('/app/data') and use relative paths only. Configure file upload middleware with fixed safe directories: multer({dest: './uploads'}) without user-controlled paths. For production, use object storage (S3, GCS, Azure Blob) with pre-signed URLs instead of direct filesystem access eliminating path traversal risks. Run application with minimal filesystem permissions - use non-root user, read-only root filesystem, restrictive AppArmor/SELinux profiles limiting file access to specific directories only.

6

Validate File Extensions Against Allowed Types

Implement strict file extension validation for upload and read operations preventing access to sensitive file types. Define allowlist of permitted extensions: const ALLOWED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt']); const ext = path.extname(filename).toLowerCase(); if (!ALLOWED_EXTENSIONS.has(ext)) reject. Use path.extname() to extract extension handling edge cases correctly. Validate against allowlist not blocklist - attackers find new dangerous extensions. For uploads, validate actual file content matches extension using magic number detection (file-type library): const fileTypeResult = await fileType.fromFile(path); if (fileTypeResult.ext !== expectedExt) reject. Prevent double-extension attacks (.pdf.exe) by checking complete filename. Store files with sanitized extensions: rename file.php.txt to safe UUID avoiding extension-based code execution. Serve uploaded files with Content-Type: application/octet-stream and Content-Disposition: attachment headers preventing browser execution.

Detect This Vulnerability in Your Code

Sourcery automatically identifies javascript non-literal filesystem filename and many other security issues in your codebase.