Path Traversal and Directory Access Vulnerabilities

High Risk File Upload & Path Traversal
path-traversaldirectory-traversalfile-accessinformation-disclosurelfidot-dot-slash

What it is

A critical vulnerability that allows attackers to access files and directories outside the intended application directory by manipulating file paths. Attackers use sequences like '../' (dot-dot-slash) to traverse up directory structures and access sensitive system files, configuration files, and other restricted resources.

const express = require('express'); const path = require('path'); const fs = require('fs'); const app = express(); // VULNERABLE: Direct path construction from user input app.get('/download/:filename', (req, res) => { const filename = req.params.filename; const filePath = path.join(__dirname, 'uploads', filename); // No validation of the filename parameter if (fs.existsSync(filePath)) { res.download(filePath); } else { res.status(404).send('File not found'); } }); // VULNERABLE: File viewer endpoint app.get('/view', (req, res) => { const { file } = req.query; const content = fs.readFileSync('./documents/' + file, 'utf8'); res.send(`
${content}
`); }); app.listen(3000); /* Attack examples: 1. GET /download/../../../etc/passwd 2. GET /download/..\\..\\..\\windows\\system32\\drivers\\etc\\hosts 3. GET /view?file=../../../../etc/shadow 4. GET /download/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd (URL encoded) */
const express = require('express'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const app = express(); // SECURE: Comprehensive path validation and file management class SecureFileHandler { constructor() { this.uploadsDir = path.resolve(__dirname, 'uploads'); this.documentsDir = path.resolve(__dirname, 'documents'); this.allowedExtensions = new Set(['.txt', '.pdf', '.jpg', '.png', '.gif']); this.maxFileSize = 10 * 1024 * 1024; // 10MB // File registry for additional security this.fileRegistry = new Map(); this.loadFileRegistry(); } // Load file registry from database or secure storage loadFileRegistry() { // In production, load from database // This maps secure tokens to actual file paths try { const files = fs.readdirSync(this.uploadsDir); files.forEach(file => { const token = crypto.randomBytes(16).toString('hex'); this.fileRegistry.set(token, { filename: file, path: path.join(this.uploadsDir, file), uploadDate: new Date() }); }); } catch (error) { console.error('Error loading file registry:', error); } } // Validate and sanitize filename validateFilename(filename) { if (!filename || typeof filename !== 'string') { throw new Error('Invalid filename provided'); } // Remove any path components const basename = path.basename(filename); // Check for dangerous patterns const dangerousPatterns = [ /\.\./, // Parent directory /[\\\/:*?"<>|]/, // Invalid filename characters /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i, // Windows reserved names /^\./ // Hidden files ]; for (const pattern of dangerousPatterns) { if (pattern.test(basename)) { throw new Error('Filename contains invalid characters'); } } // Validate extension const ext = path.extname(basename).toLowerCase(); if (!this.allowedExtensions.has(ext)) { throw new Error('File extension not allowed'); } return basename; } // Secure path resolution resolvePath(filename, baseDir) { const safeName = this.validateFilename(filename); const fullPath = path.resolve(baseDir, safeName); // Ensure resolved path is within base directory if (!fullPath.startsWith(baseDir + path.sep)) { throw new Error('Path traversal attempt detected'); } return fullPath; } // Get file by secure token getFileByToken(token) { const fileInfo = this.fileRegistry.get(token); if (!fileInfo) { throw new Error('File not found'); } // Verify file still exists and hasn't been tampered with if (!fs.existsSync(fileInfo.path)) { this.fileRegistry.delete(token); throw new Error('File no longer exists'); } return fileInfo; } // List available files (returns tokens, not paths) listFiles() { const files = []; for (const [token, info] of this.fileRegistry.entries()) { files.push({ token, filename: path.basename(info.filename), uploadDate: info.uploadDate }); } return files; } } const fileHandler = new SecureFileHandler(); // SECURE: Token-based file download app.get('/download/:token', (req, res) => { try { const { token } = req.params; // Validate token format if (!/^[a-f0-9]{32}$/.test(token)) { return res.status(400).json({ error: 'Invalid token format' }); } const fileInfo = fileHandler.getFileByToken(token); // Additional security headers res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Content-Disposition', `attachment; filename="${path.basename(fileInfo.filename)}"`); res.download(fileInfo.path, path.basename(fileInfo.filename)); } catch (error) { console.error('Download error:', error.message); res.status(404).json({ error: 'File not found' }); } }); // SECURE: Protected file viewer with validation app.get('/view/:token', (req, res) => { try { const { token } = req.params; if (!/^[a-f0-9]{32}$/.test(token)) { return res.status(400).json({ error: 'Invalid token format' }); } const fileInfo = fileHandler.getFileByToken(token); // Only allow viewing of text files const ext = path.extname(fileInfo.filename).toLowerCase(); if (ext !== '.txt') { return res.status(400).json({ error: 'File type not supported for viewing' }); } // Check file size before reading const stats = fs.statSync(fileInfo.path); if (stats.size > 1024 * 1024) { // 1MB limit for viewing return res.status(400).json({ error: 'File too large to view' }); } const content = fs.readFileSync(fileInfo.path, 'utf8'); // Escape HTML to prevent XSS const escapedContent = content .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); res.send(` File Viewer

File: ${path.basename(fileInfo.filename)}

${escapedContent}
`); } catch (error) { console.error('View error:', error.message); res.status(404).json({ error: 'File not found' }); } }); // SECURE: List available files app.get('/files', (req, res) => { try { const files = fileHandler.listFiles(); res.json({ files }); } catch (error) { console.error('List files error:', error.message); res.status(500).json({ error: 'Unable to list files' }); } }); // Error handling middleware app.use((error, req, res, next) => { console.error('Unhandled error:', error); res.status(500).json({ error: 'Internal server error' }); }); app.listen(3000, () => { console.log('Secure file server running on port 3000'); });

💡 Why This Fix Works

The vulnerable code directly uses user input in file paths, allowing directory traversal attacks. The secure version implements token-based file access, comprehensive path validation, and multiple security layers.

from flask import Flask, request, send_file, abort import os app = Flask(__name__) # VULNERABLE: Direct file serving with user input @app.route('/download/') def download_file(filename): file_path = os.path.join('uploads', filename) # Basic existence check, but no path validation if os.path.exists(file_path): return send_file(file_path, as_attachment=True) else: abort(404) # VULNERABLE: Image serving endpoint @app.route('/images/') def serve_image(filename): # Constructs path without validation image_path = f"static/images/{filename}" return send_file(image_path) # VULNERABLE: Template file access @app.route('/template') def get_template(): template_name = request.args.get('name', 'default.html') template_path = f"templates/{template_name}" with open(template_path, 'r') as f: content = f.read() return content if __name__ == '__main__': app.run(debug=True) # Attack examples: # /download/../../../etc/passwd # /images/../../../../../../etc/shadow # /template?name=../../../etc/hosts
from flask import Flask, request, send_file, abort, jsonify import os import re import hashlib import sqlite3 from pathlib import Path from urllib.parse import unquote app = Flask(__name__) class SecureFileManager: def __init__(self, base_dir): self.base_dir = Path(base_dir).resolve() self.allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt'} self.max_file_size = 10 * 1024 * 1024 # 10MB # Initialize file database self.init_db() def init_db(self): """Initialize SQLite database for file tracking""" self.conn = sqlite3.connect('files.db', check_same_thread=False) self.conn.execute(''' CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, token TEXT UNIQUE NOT NULL, filename TEXT NOT NULL, file_path TEXT NOT NULL, file_hash TEXT NOT NULL, upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') self.conn.commit() def validate_filename(self, filename): """Comprehensive filename validation""" if not filename or not isinstance(filename, str): raise ValueError("Invalid filename") # URL decode multiple times to catch double encoding decoded = filename for _ in range(3): new_decoded = unquote(decoded) if new_decoded == decoded: break decoded = new_decoded # Remove path components clean_name = os.path.basename(decoded) # Validate against dangerous patterns dangerous_patterns = [ r'\.\.', # Parent directory r'[<>:"|?*\\]', # Dangerous characters r'^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$', # Windows reserved r'^\.' # Hidden files ] for pattern in dangerous_patterns: if re.search(pattern, clean_name, re.IGNORECASE): raise ValueError(f"Filename contains invalid pattern: {pattern}") # Check extension ext = Path(clean_name).suffix.lower() if ext not in self.allowed_extensions: raise ValueError(f"Extension {ext} not allowed") # Length validation if len(clean_name) > 255 or len(clean_name) == 0: raise ValueError("Invalid filename length") return clean_name def secure_path_join(self, filename): """Securely join filename with base directory""" safe_name = self.validate_filename(filename) # Create full path full_path = (self.base_dir / safe_name).resolve() # Ensure path is within base directory try: full_path.relative_to(self.base_dir) except ValueError: raise ValueError("Path traversal attempt detected") return full_path def register_file(self, filename, file_path): """Register file in database and return access token""" # Generate secure token token = hashlib.sha256( f"{filename}{file_path}{os.urandom(32)}".encode() ).hexdigest()[:32] # Calculate file hash for integrity with open(file_path, 'rb') as f: file_hash = hashlib.sha256(f.read()).hexdigest() # Store in database self.conn.execute( 'INSERT INTO files (token, filename, file_path, file_hash) VALUES (?, ?, ?, ?)', (token, filename, str(file_path), file_hash) ) self.conn.commit() return token def get_file_by_token(self, token): """Retrieve file information by token""" if not re.match(r'^[a-f0-9]{32}$', token): raise ValueError("Invalid token format") cursor = self.conn.execute( 'SELECT filename, file_path, file_hash FROM files WHERE token = ?', (token,) ) row = cursor.fetchone() if not row: raise FileNotFoundError("File not found") filename, file_path, stored_hash = row # Verify file still exists if not os.path.exists(file_path): raise FileNotFoundError("File no longer exists") # Verify file integrity with open(file_path, 'rb') as f: current_hash = hashlib.sha256(f.read()).hexdigest() if current_hash != stored_hash: raise ValueError("File integrity check failed") return { 'filename': filename, 'path': file_path, 'hash': stored_hash } def list_files(self): """List all registered files""" cursor = self.conn.execute( 'SELECT token, filename, upload_date FROM files ORDER BY upload_date DESC' ) return [{ 'token': row[0], 'filename': row[1], 'upload_date': row[2] } for row in cursor.fetchall()] # Initialize secure file manager file_manager = SecureFileManager('uploads') # SECURE: Token-based file download @app.route('/download/') def download_file(token): try: file_info = file_manager.get_file_by_token(token) # Security headers response = send_file( file_info['path'], as_attachment=True, download_name=file_info['filename'] ) response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' return response except (ValueError, FileNotFoundError) as e: app.logger.warning(f"Download attempt failed: {e}") abort(404) except Exception as e: app.logger.error(f"Download error: {e}") abort(500) # SECURE: Protected image serving @app.route('/images/') def serve_image(token): try: file_info = file_manager.get_file_by_token(token) # Verify it's an image file ext = Path(file_info['filename']).suffix.lower() if ext not in {'.jpg', '.jpeg', '.png', '.gif'}: abort(400) response = send_file(file_info['path']) response.headers['X-Content-Type-Options'] = 'nosniff' return response except (ValueError, FileNotFoundError): abort(404) except Exception as e: app.logger.error(f"Image serve error: {e}") abort(500) # SECURE: Template access with whitelist @app.route('/template') def get_template(): # Whitelist of allowed templates allowed_templates = { 'default': 'default.html', 'login': 'login.html', 'dashboard': 'dashboard.html' } template_key = request.args.get('name', 'default') if template_key not in allowed_templates: abort(400) template_file = allowed_templates[template_key] template_path = file_manager.secure_path_join(template_file) try: with open(template_path, 'r', encoding='utf-8') as f: content = f.read() return content except FileNotFoundError: abort(404) except Exception as e: app.logger.error(f"Template error: {e}") abort(500) # File listing endpoint @app.route('/files') def list_files(): try: files = file_manager.list_files() return jsonify({'files': files}) except Exception as e: app.logger.error(f"File listing error: {e}") return jsonify({'error': 'Unable to list files'}), 500 # Error handlers @app.errorhandler(404) def not_found(error): return jsonify({'error': 'Resource not found'}), 404 @app.errorhandler(500) def internal_error(error): return jsonify({'error': 'Internal server error'}), 500 if __name__ == '__main__': app.run(debug=False) # Never run with debug=True in production

💡 Why This Fix Works

The vulnerable Flask application directly uses user input in file paths. The secure version implements a token-based system with comprehensive validation, file integrity checking, and proper error handling.

// VULNERABLE: Direct file access with user input import java.io.*; import java.nio.file.*; import javax.servlet.http.*; public class FileServlet extends HttpServlet { private static final String UPLOAD_DIR = "uploads"; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String filename = request.getParameter("file"); // VULNERABLE: Direct path construction File file = new File(UPLOAD_DIR + File.separator + filename); if (file.exists()) { // Set response headers response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=" + filename); // Stream file content try (InputStream is = new FileInputStream(file); OutputStream os = response.getOutputStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } } } else { response.sendError(HttpServletResponse.SC_NOT_FOUND); } } } // Attack examples: // ?file=../../../etc/passwd // ?file=..\\..\\..\\windows\\system32\\drivers\\etc\\hosts
// SECURE: Comprehensive file access protection import java.io.*; import java.nio.file.*; import java.security.MessageDigest; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import javax.servlet.http.*; public class SecureFileServlet extends HttpServlet { private static final Path BASE_DIR = Paths.get("uploads").toAbsolutePath(); private static final Set ALLOWED_EXTENSIONS = Set.of( ".txt", ".pdf", ".jpg", ".jpeg", ".png", ".gif" ); private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB private static final Pattern SAFE_FILENAME = Pattern.compile("^[a-zA-Z0-9._-]+$"); // File registry for token-based access private static final Map fileRegistry = new ConcurrentHashMap<>(); static class FileInfo { final String filename; final Path path; final String hash; final long uploadTime; FileInfo(String filename, Path path, String hash) { this.filename = filename; this.path = path; this.hash = hash; this.uploadTime = System.currentTimeMillis(); } } @Override public void init() throws ServletException { try { // Ensure base directory exists and is secure if (!Files.exists(BASE_DIR)) { Files.createDirectories(BASE_DIR); } // Set secure permissions (Unix systems) if (!System.getProperty("os.name").toLowerCase().contains("windows")) { Files.setPosixFilePermissions(BASE_DIR, Set.of( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE ) ); } // Load existing files into registry loadFileRegistry(); } catch (IOException e) { throw new ServletException("Failed to initialize secure file servlet", e); } } private void loadFileRegistry() throws IOException { try (DirectoryStream stream = Files.newDirectoryStream(BASE_DIR)) { for (Path file : stream) { if (Files.isRegularFile(file)) { String filename = file.getFileName().toString(); if (isValidFilename(filename)) { String token = generateToken(filename, file); String hash = calculateFileHash(file); fileRegistry.put(token, new FileInfo(filename, file, hash)); } } } } } private boolean isValidFilename(String filename) { if (filename == null || filename.trim().isEmpty()) { return false; } // Check length if (filename.length() > 255) { return false; } // Check pattern if (!SAFE_FILENAME.matcher(filename).matches()) { return false; } // Check extension String extension = getFileExtension(filename).toLowerCase(); if (!ALLOWED_EXTENSIONS.contains(extension)) { return false; } // Check for dangerous names (Windows) String baseName = filename.toLowerCase(); if (baseName.matches("^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\..*)?$")) { return false; } return true; } private String getFileExtension(String filename) { int lastDot = filename.lastIndexOf('.'); return lastDot >= 0 ? filename.substring(lastDot) : ""; } private String generateToken(String filename, Path path) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); String input = filename + path.toString() + System.nanoTime(); byte[] hash = md.digest(input.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 16; i++) { // Use first 16 bytes for 32 char hex sb.append(String.format("%02x", hash[i] & 0xff)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException("Failed to generate token", e); } } private String calculateFileHash(Path file) throws IOException { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(Files.readAllBytes(file)); StringBuilder sb = new StringBuilder(); for (byte b : hash) { sb.append(String.format("%02x", b & 0xff)); } return sb.toString(); } catch (Exception e) { throw new IOException("Failed to calculate file hash", e); } } private Path validateAndResolvePath(String userInput) throws SecurityException { if (userInput == null || userInput.trim().isEmpty()) { throw new SecurityException("Invalid file input"); } // Sanitize input String sanitized = userInput.replaceAll("[^a-zA-Z0-9._-]", ""); if (!isValidFilename(sanitized)) { throw new SecurityException("Invalid filename: " + sanitized); } // Resolve path Path requestedPath = BASE_DIR.resolve(sanitized).normalize(); // Ensure path is within base directory if (!requestedPath.startsWith(BASE_DIR)) { throw new SecurityException("Path traversal attempt detected"); } // Check if file exists if (!Files.exists(requestedPath)) { throw new SecurityException("File not found"); } // Ensure it's a regular file if (!Files.isRegularFile(requestedPath)) { throw new SecurityException("Path does not point to a regular file"); } return requestedPath; } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String token = request.getParameter("token"); try { // Validate token format if (token == null || !token.matches("^[a-f0-9]{32}$")) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid token format"); return; } // Get file info from registry FileInfo fileInfo = fileRegistry.get(token); if (fileInfo == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found"); return; } // Verify file still exists if (!Files.exists(fileInfo.path)) { fileRegistry.remove(token); // Clean up registry response.sendError(HttpServletResponse.SC_NOT_FOUND, "File no longer exists"); return; } // Verify file integrity String currentHash = calculateFileHash(fileInfo.path); if (!currentHash.equals(fileInfo.hash)) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "File integrity check failed"); return; } // Check file size long fileSize = Files.size(fileInfo.path); if (fileSize > MAX_FILE_SIZE) { response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "File too large"); return; } // Set security headers response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Content-Security-Policy", "default-src 'none'"); // Set content headers response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileInfo.filename + "\""); response.setContentLengthLong(fileSize); // Stream file content try (InputStream is = Files.newInputStream(fileInfo.path); OutputStream os = response.getOutputStream()) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } } // Log access for security monitoring log("File accessed: " + fileInfo.filename + " by " + request.getRemoteAddr()); } catch (SecurityException e) { log("Security violation: " + e.getMessage() + " from " + request.getRemoteAddr()); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied"); } catch (Exception e) { log("Error serving file: " + e.getMessage()); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error"); } } // List available files (for admin interface) protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if ("/list".equals(request.getPathInfo())) { response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); StringBuilder json = new StringBuilder("[]"); if (!fileRegistry.isEmpty()) { json = new StringBuilder("["); boolean first = true; for (Map.Entry entry : fileRegistry.entrySet()) { if (!first) json.append(","); json.append("{"); json.append("\"token\":\"").append(entry.getKey()).append("\","); json.append("\"filename\":\"").append(entry.getValue().filename).append("\","); json.append("\"uploadTime\":").append(entry.getValue().uploadTime); json.append("}"); first = false; } json.append("]"); } response.getWriter().write(json.toString()); } else { response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } } }

💡 Why This Fix Works

The vulnerable Java servlet directly constructs file paths from user input. The secure version implements token-based access, comprehensive validation, file integrity checking, and proper security headers.

Why it happens

The most common cause is directly using user-provided input to construct file paths without proper validation or sanitization. Applications that accept filenames, document IDs, or path parameters from users and use them directly in file operations are vulnerable to path traversal attacks that can access any file on the system.

Root causes

Unvalidated User Input in File Paths

The most common cause is directly using user-provided input to construct file paths without proper validation or sanitization. Applications that accept filenames, document IDs, or path parameters from users and use them directly in file operations are vulnerable to path traversal attacks that can access any file on the system.

Preview example – JAVASCRIPT
// VULNERABLE: Direct user input in file path
app.get('/download/:filename', (req, res) => {
    const filename = req.params.filename;
    const filePath = './uploads/' + filename;
    
    // Attacker input: "../../../etc/passwd"
    // Results in: ./uploads/../../../etc/passwd
    // Which resolves to: /etc/passwd
    res.sendFile(path.resolve(filePath));
});

Insufficient Path Normalization

Applications that fail to properly normalize file paths before validation can be bypassed using various encoding techniques, double dots, or mixed separators. Attackers can use URL encoding (%2e%2e%2f), Unicode encoding, or platform-specific path separators to bypass basic validation filters.

Preview example – PHP
<?php
// VULNERABLE: Basic validation can be bypassed
function getFile($filename) {
    // Weak validation - only checks for obvious patterns
    if (strpos($filename, '../') !== false) {
        die('Invalid filename');
    }
    
    $filePath = 'files/' . $filename;
    
    // Bypass examples:
    // "..\\..\\..\\etc\\passwd" (Windows-style separators)
    // "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd" (URL encoded)
    // "....//....//....//etc/passwd" (double dots)
    return file_get_contents($filePath);
}

Operating System Path Handling Differences

Different operating systems handle file paths differently, creating opportunities for bypasses. Windows systems use backslashes, support alternate data streams, and have different case sensitivity rules. Attackers can exploit these differences to bypass validation that only considers one platform's path conventions.

Preview example – PYTHON
# VULNERABLE: Platform-specific path handling issues
def read_file(filename):
    # Only validates forward slashes, but Windows accepts both
    if '/' in filename and '..' in filename:
        raise ValueError('Invalid path')
    
    file_path = os.path.join('data', filename)
    
    # Bypass on Windows:
    # "..\\..\\..\\windows\\system32\\drivers\\etc\\hosts"
    # "file.txt:alternate_stream" (NTFS alternate data streams)
    with open(file_path, 'r') as f:
        return f.read()

Symbolic Link and Junction Point Attacks

Applications that don't properly handle symbolic links, hard links, or Windows junction points can be exploited to access files outside the intended directory. Even when path validation is performed, symbolic links can redirect file operations to sensitive system locations, effectively bypassing directory restrictions.

Preview example – BASH
#!/bin/bash
# VULNERABLE: Following symbolic links without validation

# Attacker creates symbolic link in upload directory:
# ln -s /etc/passwd uploads/innocent_file.txt

# Application code:
function serve_file() {
    local filename="$1"
    local file_path="uploads/$filename"
    
    # Even with basic validation, symlinks bypass restrictions
    if [[ "$filename" =~ \.\./ ]]; then
        echo "Invalid filename"
        return 1
    fi
    
    # This follows the symlink and serves /etc/passwd
    cat "$file_path"
}

Fixes

1

Implement Robust Path Validation and Canonicalization

Always canonicalize and validate file paths before use. Convert all paths to their absolute canonical form, remove redundant elements, and verify they remain within allowed directories. Use platform-specific path handling functions and validate against a whitelist of allowed paths or patterns.

View implementation – JAVASCRIPT
// SECURE: Comprehensive path validation
const path = require('path');
const fs = require('fs');

function secureFileAccess(userPath, allowedDir) {
    try {
        // Canonicalize paths to resolve .., ., and symbolic links
        const canonicalUserPath = path.resolve(userPath);
        const canonicalAllowedDir = path.resolve(allowedDir);
        
        // Verify the resolved path is within allowed directory
        if (!canonicalUserPath.startsWith(canonicalAllowedDir + path.sep)) {
            throw new Error('Access denied: Path outside allowed directory');
        }
        
        // Additional validation
        if (!fs.existsSync(canonicalUserPath)) {
            throw new Error('File not found');
        }
        
        // Check if it's actually a file (not a directory)
        const stats = fs.statSync(canonicalUserPath);
        if (!stats.isFile()) {
            throw new Error('Path does not point to a file');
        }
        
        return canonicalUserPath;
    } catch (error) {
        throw new Error('Invalid file path: ' + error.message);
    }
}
2

Use Secure File APIs and Abstractions

Leverage secure file handling APIs that provide built-in path validation and sandboxing. Use file access abstractions that work with file IDs or tokens rather than direct paths, implement chroot jails or containerization, and avoid exposing file system structure to users.

View implementation – PHP
<?php
// SECURE: File access through secure abstraction
class SecureFileManager {
    private $baseDir;
    private $allowedExtensions = ['txt', 'pdf', 'jpg', 'png'];
    
    public function __construct($baseDir) {
        $this->baseDir = realpath($baseDir);
        if (!$this->baseDir) {
            throw new Exception('Invalid base directory');
        }
    }
    
    public function getFileById($fileId) {
        // Use database lookup instead of direct path construction
        $stmt = $this->db->prepare('SELECT filename, path FROM files WHERE id = ? AND user_id = ?');
        $stmt->execute([$fileId, $this->getCurrentUserId()]);
        $fileInfo = $stmt->fetch();
        
        if (!$fileInfo) {
            throw new Exception('File not found');
        }
        
        return $this->validateAndGetFile($fileInfo['path']);
    }
    
    private function validateAndGetFile($filename) {
        // Sanitize filename
        $filename = basename($filename); // Remove any path components
        
        // Validate extension
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        if (!in_array($extension, $this->allowedExtensions)) {
            throw new Exception('File type not allowed');
        }
        
        // Construct and validate full path
        $fullPath = $this->baseDir . DIRECTORY_SEPARATOR . $filename;
        $realPath = realpath($fullPath);
        
        // Ensure file exists and is within base directory
        if (!$realPath || strpos($realPath, $this->baseDir) !== 0) {
            throw new Exception('Invalid file path');
        }
        
        return $realPath;
    }
}
3

Implement Input Sanitization and Encoding

Thoroughly sanitize all user inputs used in file operations. Decode URL encoding, normalize Unicode characters, remove null bytes, and validate against strict character whitelists. Implement multiple layers of encoding/decoding to catch sophisticated bypass attempts.

View implementation – PYTHON
import os
import urllib.parse
import unicodedata
import re

def sanitize_filename(user_input):
    """Comprehensive filename sanitization"""
    
    # Step 1: URL decode multiple times to catch double-encoding
    decoded = user_input
    for _ in range(3):  # Decode up to 3 times
        try:
            new_decoded = urllib.parse.unquote(decoded)
            if new_decoded == decoded:
                break
            decoded = new_decoded
        except:
            break
    
    # Step 2: Unicode normalization
    normalized = unicodedata.normalize('NFKC', decoded)
    
    # Step 3: Remove null bytes and control characters
    cleaned = ''.join(char for char in normalized if ord(char) >= 32)
    
    # Step 4: Remove dangerous path components
    dangerous_patterns = [
        r'\.\.',  # Parent directory
        r'[\\/:]',  # Path separators and drive letters
        r'[<>:"|?*]',  # Windows forbidden characters
        r'^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$'  # Windows reserved names
    ]
    
    for pattern in dangerous_patterns:
        cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
    
    # Step 5: Whitelist allowed characters
    allowed_chars = re.compile(r'^[a-zA-Z0-9._-]+$')
    if not allowed_chars.match(cleaned):
        raise ValueError('Filename contains invalid characters')
    
    # Step 6: Length validation
    if len(cleaned) == 0 or len(cleaned) > 255:
        raise ValueError('Invalid filename length')
    
    return cleaned

def secure_file_path(filename, base_dir):
    """Create secure file path with validation"""
    
    # Sanitize filename
    safe_filename = sanitize_filename(filename)
    
    # Create full path
    full_path = os.path.join(base_dir, safe_filename)
    
    # Resolve and validate path
    try:
        canonical_path = os.path.realpath(full_path)
        canonical_base = os.path.realpath(base_dir)
        
        # Ensure path is within base directory
        if not canonical_path.startswith(canonical_base + os.sep):
            raise ValueError('Path traversal attempt detected')
        
        return canonical_path
    except OSError:
        raise ValueError('Invalid file path')
4

Configure System-Level Protections

Implement defense-in-depth with system-level protections including chroot jails, containerization, file system permissions, SELinux/AppArmor policies, and monitoring. Use dedicated user accounts with minimal privileges and implement file access logging for security monitoring.

View implementation – DOCKERFILE
# Docker containerization for file isolation
FROM alpine:latest

# Create non-privileged user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set up restricted directory structure
RUN mkdir -p /app/uploads /app/temp && \
    chown -R appuser:appgroup /app && \
    chmod 755 /app && \
    chmod 700 /app/uploads /app/temp

# Install and configure file access monitoring
RUN apk add --no-cache auditd

# Audit rules for file access monitoring
ECHO '-w /app/uploads -p wa -k file_access' >> /etc/audit/audit.rules
ECHO '-w /etc/passwd -p wa -k sensitive_files' >> /etc/audit/audit.rules

# SELinux policy (on RHEL/CentOS)
# setsebool -P httpd_read_user_content 1
# semanage fcontext -a -t httpd_exec_t "/app/uploads(/.*)?"
# restorecon -R /app/uploads

# AppArmor profile (on Ubuntu/Debian)
# /etc/apparmor.d/usr.bin.myapp:
# #include <tunables/global>
# /usr/bin/myapp {
#   #include <abstractions/base>
#   /app/uploads/** r,
#   /app/temp/** rw,
#   deny /etc/passwd r,
#   deny /etc/shadow r,
#   deny /root/** rwx,
# }

USER appuser
WORKDIR /app

# File system mount options for additional security
# mount -o noexec,nosuid,nodev /dev/sdb1 /app/uploads

Detect This Vulnerability in Your Code

Sourcery automatically identifies path traversal and directory access vulnerabilities and many other security issues in your codebase.