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(`<pre>${content}</pre>`);
});

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, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#x27;');
        
        res.send(`
            <!DOCTYPE html>
            <html>
            <head>
                <title>File Viewer</title>
                <meta charset="utf-8">
                <style>
                    body { font-family: monospace; margin: 20px; }
                    pre { background: #f5f5f5; padding: 15px; border-radius: 5px; }
                </style>
            </head>
            <body>
                <h2>File: ${path.basename(fileInfo.filename)}</h2>
                <pre>${escapedContent}</pre>
            </body>
            </html>
        `);
        
    } 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/<path:filename>')
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/<filename>')
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/<token>')
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/<token>')
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<String> 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<String, FileInfo> 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<Path> 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<String, FileInfo> 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.