JavaScript Path Traversal via path.join/resolve

High Risk Path Traversal
javascriptpath-traversalpath-joinpath-resolvedirectory-traversalfile-access

What it is

The JavaScript application uses path.join() or path.resolve() with user-controlled input without proper validation, enabling path traversal attacks. Attackers can manipulate file paths to access files outside the intended directory structure using sequences like '../' to traverse up the directory tree.

// Vulnerable: Unvalidated path.join() usage
const fs = require('fs');
const path = require('path');

app.get('/files/*', (req, res) => {
  const requestedPath = req.params[0];
  
  // Dangerous: User input directly in path.join
  const filePath = path.join(__dirname, 'public', requestedPath);
  
  fs.readFile(filePath, 'utf8', (err, data) => {
    if (err) {
      return res.status(404).send('File not found');
    }
    res.send(data);
  });
});
// Secure: Path validation with resolve and boundary checking
const fs = require('fs');
const path = require('path');

const PUBLIC_DIR = path.resolve(__dirname, 'public');

app.get('/files/*', (req, res) => {
  const requestedPath = req.params[0];
  
  // Validate input format
  if (!requestedPath || typeof requestedPath !== 'string') {
    return res.status(400).json({ error: 'Invalid path' });
  }
  
  // Remove any traversal attempts
  const safePath = requestedPath.replace(/\.\.\/|\.\.\\\/g, '');
  
  // Resolve the full path
  const filePath = path.resolve(PUBLIC_DIR, safePath);
  
  // Ensure the resolved path is within the public directory
  if (!filePath.startsWith(PUBLIC_DIR + path.sep) && filePath !== PUBLIC_DIR) {
    return res.status(403).json({ error: 'Access denied' });
  }
  
  fs.readFile(filePath, 'utf8', (err, data) => {
    if (err) {
      return res.status(404).json({ error: 'File not found' });
    }
    res.send(data);
  });
});

💡 Why This Fix Works

The secure version validates and normalizes the path, removes traversal sequences, resolves the full path, and ensures it stays within the allowed directory boundary.

Why it happens

Applications use Node.js path.join() with user-controlled input believing it provides automatic security against path traversal: const filePath = path.join(baseDir, req.query.file). However, path.join() only normalizes path separators and resolves . and .. - it doesn't restrict traversal outside the base directory. Attackers provide input like '../../../etc/passwd' and path.join() produces valid traversal paths. Developers mistakenly think path.join() is a security function when it's purely for cross-platform path construction. Absolute paths passed to path.join() can completely replace the base path: path.join('/safe', '/etc/passwd') returns /etc/passwd on Unix. Even with path.join(), applications must validate the resulting path stays within intended boundaries.

Root causes

Using path.join() with Unvalidated User Input

Applications use Node.js path.join() with user-controlled input believing it provides automatic security against path traversal: const filePath = path.join(baseDir, req.query.file). However, path.join() only normalizes path separators and resolves . and .. - it doesn't restrict traversal outside the base directory. Attackers provide input like '../../../etc/passwd' and path.join() produces valid traversal paths. Developers mistakenly think path.join() is a security function when it's purely for cross-platform path construction. Absolute paths passed to path.join() can completely replace the base path: path.join('/safe', '/etc/passwd') returns /etc/passwd on Unix. Even with path.join(), applications must validate the resulting path stays within intended boundaries.

Improper Use of path.resolve() with External Data

Code uses path.resolve() to construct absolute paths from user input without validating the result: const fullPath = path.resolve(baseDirectory, userInput); fs.readFile(fullPath). While path.resolve() converts paths to absolute form, it doesn't prevent directory traversal. If userInput contains ../, the resolved path can traverse outside base directory. path.resolve() with absolute input paths ignores all previous path segments: path.resolve('/home/app', '/etc/passwd') returns /etc/passwd. Applications use path.resolve() for path canonicalization but skip the critical security step of verifying resolved path is within allowed directory. Combining path.resolve() with user input without subsequent validation creates exploitable path traversal vulnerabilities.

Missing Path Normalization Before Security Validation

Applications validate paths for traversal sequences but don't normalize paths first, allowing encoding bypasses. Code checks for ../ in user input: if (userPath.includes('..')) reject - but misses URL-encoded variants (%2e%2e%2f), double-encoded (%252e%252e%252f), or mixed encodings (..%2f). Windows path separators (\) bypass checks looking only for forward slashes. Unicode normalization differences allow bypasses. Applications validate user input string directly without using path.normalize() to canonicalize first. Path validation must occur after normalization: const normalized = path.normalize(userPath); then validate normalized path. Validation on raw user input before normalization creates exploitable gaps where encoded traversal sequences bypass security checks.

Insufficient Filtering of Directory Traversal Sequences

Applications attempt to filter dangerous path components using string replacement or regex but use incomplete or bypassable patterns. Code like userPath.replace(/\.\.\/g, '') removes ../ but gets bypassed with ..././ which becomes ../ after replacement. Checks using startsWith('../') miss traversal in middle of path: 'safe/../../../etc/passwd'. Blocklists try to enumerate dangerous patterns but miss variations: Windows paths (..\), absolute paths (/etc/passwd), null bytes (\0 truncating paths in C libraries), double slashes (//) normalizing to single slash. Regex patterns like /\.\./ match literal dots but don't account for path.join() resolving them. Filtering approach fundamentally flawed - should use allowlist validation of resolved absolute paths instead.

Trusting User-Provided Relative Paths Without Verification

Applications accept user-provided relative paths assuming they're safe because they don't look like obvious traversal attempts. Code trusts paths like 'uploads/profile.jpg' or 'documents/report.pdf' without considering user can provide 'uploads/../config/database.yml'. Developers assume filesystem will reject invalid paths without understanding path traversal allows access to any file. Multi-segment paths combined from multiple user inputs create traversal: path.join(userFolder, userSubfolder, userFilename) where each component seems safe but combined enables traversal. No verification that final path after path.join/resolve remains within intended directory. Applications implement user file isolation using directory structure (users/userA/, users/userB/) but don't verify users can't traverse into other user directories using relative paths.

Fixes

1

Validate All Path Components Before path.join()

Validate each path component before using path.join() to combine them. Reject components containing ../ sequences, null bytes, or path separators: const SAFE_COMPONENT = /^[a-zA-Z0-9_\-\.]+$/; if (!SAFE_COMPONENT.test(component)) throw new Error('Invalid path component'). Validate each segment individually: path.join(base, validateComponent(dir), validateComponent(file)). For paths with multiple segments from user input, split and validate each: const segments = userPath.split('/').filter(s => s && s !== '.' && s !== '..'); if (segments.length !== userPath.split('/').length) reject. Use allowlist of permitted characters rather than trying to filter dangerous patterns. After joining, still validate the final path using path.resolve() and directory containment checks for defense-in-depth.

2

Use path.resolve() and Validate Directory Containment

Always use path.resolve() to get absolute canonical path, then validate it stays within allowed directory: const safePath = path.resolve(baseDir, userInput); const safeBase = path.resolve(baseDir); if (!safePath.startsWith(safeBase + path.sep)) throw new Error('Path traversal detected'). Using path.sep ensures proper validation: safeBase + path.sep prevents prefix attacks where /uploads and /uploads-evil both start with /uploads. Alternative validation using path.relative(): const relative = path.relative(safeBase, safePath); if (relative.startsWith('..') || path.isAbsolute(relative)) reject. Test validation with traversal attempts: '../', absolute paths, Windows paths, symlinks. Implement as reusable utility function used consistently across application for all file operations.

3

Implement Strict Allowlists for Path Components

Create allowlists of permitted directories, filenames, or path patterns rather than blocklisting dangerous patterns. Define allowed subdirectories: const ALLOWED_DIRS = new Set(['uploads', 'public', 'temp']); validate directory is in set. For filenames, use allowlist regex: /^[a-zA-Z0-9_\-]{1,255}\.(jpg|png|pdf)$/. Implement indirect object references where users select from predefined options (dropdown of valid paths) rather than providing arbitrary paths. Use database mapping of user IDs to allowed directories: SELECT base_dir FROM user_storage WHERE user_id = ? then restrict access to that directory. For multi-tenant applications, enforce tenant-specific directory isolation through allowlists preventing cross-tenant access. Never attempt to sanitize paths - reject invalid input requiring users to provide valid paths within allowlist constraints.

4

Normalize Paths Before Validation and Usage

Always normalize paths using path.normalize() before validation to resolve . and .. sequences and handle encoding: const normalized = path.normalize(decodeURIComponent(userPath)). Decode URL encoding before normalization to catch encoded traversal attempts. Use path.resolve() after normalization to get absolute paths: const absolute = path.resolve(baseDir, normalized). Normalize before any security checks including validation regex, startsWith checks, or allowlist matching. Be aware path.normalize() alone doesn't prevent traversal - it's preprocessing step before validation. For Windows compatibility, handle backslash separators: normalized.replace(/\\/g, '/'). Validate normalized absolute path against base directory using string prefix checks with path.sep or path.relative() approach. Log original and normalized paths for security monitoring to detect traversal attempts.

5

Implement Filesystem Isolation and Access Restrictions

Deploy application with filesystem-level restrictions preventing access outside designated directories even if path validation fails. Use Docker containers with volume mounts restricting filesystem visibility: docker run -v /app/data:/data:rw app only mounts /app/data directory. Configure Linux capabilities, AppArmor, or SELinux profiles restricting file access to specific paths. Run application as non-root user with minimal filesystem permissions using chmod/chown. Use chroot jail or systemd DynamicUser restricting process view of filesystem. For production, replace direct filesystem access with object storage (S3, Azure Blob, GCS) using pre-signed URLs eliminating path traversal attack surface. Implement separate storage backend per tenant in multi-tenant systems with access control enforced at infrastructure level.

6

Replace Direct File Paths with Indirect Object References

Architectural change eliminating path traversal by never accepting user-provided file paths. Assign opaque IDs to files storing actual paths server-side: INSERT INTO files (id, path, owner_id) VALUES (uuid(), actualPath, userId). Users reference files by ID: GET /files/{fileId} retrieves file using database lookup with access control: SELECT path FROM files WHERE id = ? AND owner_id = ?. Generate server-controlled filenames for uploads: const filename = crypto.randomUUID() + path.extname(originalName); save to database with original name for display purposes. This pattern eliminates path traversal entirely as users never control paths. Implement access control in database layer enforcing ownership and permissions. Use database transactions ensuring file metadata and actual files remain synchronized.

Detect This Vulnerability in Your Code

Sourcery automatically identifies javascript path traversal via path.join/resolve and many other security issues in your codebase.