Path Traversal from HTTP Request Data in File Access in Spring

High Risk Path Traversal
JavaSpringPath TraversalFile AccessDirectory TraversalLocal File Inclusion

What it is

Applications that use untrusted HTTP request data directly as file paths without proper validation are vulnerable to path traversal attacks, allowing attackers to read, write, or delete arbitrary files on the server by using directory traversal sequences like '../' or absolute paths.

import org.springframework.web.bind.annotation.*;
import org.springframework.core.io.Resource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ResponseEntity;
import java.nio.file.*;
import java.io.IOException;

@RestController
public class VulnerableFileController {
    
    private static final String BASE_DIR = "/var/app/files/";
    
    // Vulnerable: Direct path concatenation
    @GetMapping("/download/{filename}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
        String fullPath = BASE_DIR + filename; // Vulnerable to path traversal
        Resource resource = new FileSystemResource(fullPath);
        
        if (resource.exists()) {
            return ResponseEntity.ok(resource);
        }
        return ResponseEntity.notFound().build();
    }
    
    // Vulnerable: User-controlled directory traversal
    @GetMapping("/files/{category}/{filename}")
    public ResponseEntity<String> readFile(
            @PathVariable String category,
            @PathVariable String filename) {
        
        try {
            // Vulnerable: Both parameters are user-controlled
            String path = BASE_DIR + category + "/" + filename;
            String content = Files.readString(Paths.get(path));
            return ResponseEntity.ok(content);
        } catch (IOException e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    // Vulnerable: Insufficient validation
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam("path") String targetPath) {
        
        // Weak validation that can be bypassed
        if (targetPath.contains("..")) {
            return ResponseEntity.badRequest().body("Invalid path");
        }
        
        try {
            // Still vulnerable to encoded traversal sequences
            String fullPath = BASE_DIR + targetPath + "/" + file.getOriginalFilename();
            Files.copy(file.getInputStream(), Paths.get(fullPath));
            return ResponseEntity.ok("File uploaded");
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body("Upload failed");
        }
    }
    
    // Vulnerable: No path validation in service method
    @GetMapping("/user-files/{userId}/{document}")
    public ResponseEntity<Resource> getUserDocument(
            @PathVariable String userId,
            @PathVariable String document) {
        
        // Attacker can use userId="../../../etc" and document="passwd"
        String userDir = BASE_DIR + "users/" + userId + "/";
        String documentPath = userDir + document;
        
        Resource resource = new FileSystemResource(documentPath);
        return ResponseEntity.ok(resource);
    }
}
import org.springframework.web.bind.annotation.*;
import org.springframework.core.io.Resource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import java.nio.file.*;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;

@RestController
public class SecureFileController {
    
    private static final Path BASE_DIR = Paths.get("/var/app/files").toAbsolutePath().normalize();
    private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
        ".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".docx"
    );
    
    @Autowired
    private SecureFilePathUtil filePathUtil;
    
    // Secure: Proper path validation and normalization
    @GetMapping("/download/{filename}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
        
        // Validate filename
        if (!filePathUtil.isValidFilename(filename)) {
            return ResponseEntity.badRequest().build();
        }
        
        // Check allowed extensions
        String extension = getFileExtension(filename).toLowerCase();
        if (!ALLOWED_EXTENSIONS.contains(extension)) {
            return ResponseEntity.badRequest().build();
        }
        
        // Securely resolve path
        Optional<Path> securePathOpt = filePathUtil.resolveSecurePath(BASE_DIR, filename);
        if (securePathOpt.isEmpty()) {
            return ResponseEntity.badRequest().build();
        }
        
        Path securePath = securePathOpt.get();
        
        try {
            if (!Files.exists(securePath) || !Files.isRegularFile(securePath)) {
                return ResponseEntity.notFound().build();
            }
            
            Resource resource = new FileSystemResource(securePath);
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                    "attachment; filename=\"" + filename + "\"")
                .body(resource);
                
        } catch (Exception e) {
            logger.error("File access error: {}", filename, e);
            return ResponseEntity.badRequest().build();
        }
    }
    
    // Secure: Category validation with allowlist
    @GetMapping("/files/{category}/{filename}")
    public ResponseEntity<String> readFile(
            @PathVariable String category,
            @PathVariable String filename) {
        
        // Validate category against allowlist
        Set<String> allowedCategories = Set.of("documents", "images", "reports");
        if (!allowedCategories.contains(category)) {
            return ResponseEntity.badRequest().build();
        }
        
        // Validate filename
        if (!filePathUtil.isValidFilename(filename)) {
            return ResponseEntity.badRequest().build();
        }
        
        try {
            // Securely resolve category directory
            Optional<Path> categoryPathOpt = filePathUtil.resolveSecurePath(BASE_DIR, category);
            if (categoryPathOpt.isEmpty()) {
                return ResponseEntity.badRequest().build();
            }
            
            // Securely resolve file within category
            Optional<Path> filePathOpt = filePathUtil.resolveSecurePath(categoryPathOpt.get(), filename);
            if (filePathOpt.isEmpty()) {
                return ResponseEntity.badRequest().build();
            }
            
            Path filePath = filePathOpt.get();
            if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
                return ResponseEntity.notFound().build();
            }
            
            // Check file size limit
            if (Files.size(filePath) > 1024 * 1024) { // 1MB limit
                return ResponseEntity.badRequest().body("File too large");
            }
            
            String content = Files.readString(filePath);
            return ResponseEntity.ok(content);
            
        } catch (IOException e) {
            logger.error("File read error: {}/{}", category, filename, e);
            return ResponseEntity.internalServerError().build();
        }
    }
    
    // Secure: Multiple layers of validation
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam("category") String category) {
        
        // Validate file
        if (file.isEmpty() || file.getSize() > 10 * 1024 * 1024) { // 10MB limit
            return ResponseEntity.badRequest().body("Invalid file size");
        }
        
        String originalFilename = file.getOriginalFilename();
        if (!filePathUtil.isValidFilename(originalFilename)) {
            return ResponseEntity.badRequest().body("Invalid filename");
        }
        
        // Validate category
        Set<String> allowedCategories = Set.of("documents", "images", "uploads");
        if (!allowedCategories.contains(category)) {
            return ResponseEntity.badRequest().body("Invalid category");
        }
        
        // Check file extension
        String extension = getFileExtension(originalFilename).toLowerCase();
        if (!ALLOWED_EXTENSIONS.contains(extension)) {
            return ResponseEntity.badRequest().body("File type not allowed");
        }
        
        try {
            // Securely resolve upload directory
            Optional<Path> categoryPathOpt = filePathUtil.resolveSecurePath(BASE_DIR, category);
            if (categoryPathOpt.isEmpty()) {
                return ResponseEntity.badRequest().body("Invalid category path");
            }
            
            Path categoryPath = categoryPathOpt.get();
            Files.createDirectories(categoryPath);
            
            // Generate unique filename to prevent conflicts
            String uniqueFilename = generateUniqueFilename(originalFilename);
            
            // Securely resolve target file path
            Optional<Path> targetPathOpt = filePathUtil.resolveSecurePath(categoryPath, uniqueFilename);
            if (targetPathOpt.isEmpty()) {
                return ResponseEntity.badRequest().body("Invalid target path");
            }
            
            Path targetPath = targetPathOpt.get();
            
            // Copy file with atomic operation
            Files.copy(file.getInputStream(), targetPath, StandardCopyOption.ATOMIC_MOVE);
            
            // Set secure file permissions
            Files.setPosixFilePermissions(targetPath, 
                PosixFilePermissions.fromString("rw-r--r--"));
            
            return ResponseEntity.ok("File uploaded: " + uniqueFilename);
            
        } catch (IOException e) {
            logger.error("Upload error: {}", originalFilename, e);
            return ResponseEntity.internalServerError().body("Upload failed");
        }
    }
    
    // Secure: User isolation with proper validation
    @GetMapping("/user-files/{userId}/{document}")
    public ResponseEntity<Resource> getUserDocument(
            @PathVariable String userId,
            @PathVariable String document,
            Authentication auth) {
        
        // Authorization: Users can only access their own files
        if (!userId.equals(auth.getName()) && !hasAdminRole(auth)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
        
        // Validate user ID format (e.g., UUID or alphanumeric)
        if (!userId.matches("[a-zA-Z0-9-]{1,36}")) {
            return ResponseEntity.badRequest().build();
        }
        
        // Validate document name
        if (!filePathUtil.isValidFilename(document)) {
            return ResponseEntity.badRequest().build();
        }
        
        try {
            // Securely resolve user directory
            Optional<Path> userDirOpt = filePathUtil.resolveSecurePath(BASE_DIR, "users/" + userId);
            if (userDirOpt.isEmpty()) {
                return ResponseEntity.badRequest().build();
            }
            
            // Securely resolve document path
            Optional<Path> documentPathOpt = filePathUtil.resolveSecurePath(userDirOpt.get(), document);
            if (documentPathOpt.isEmpty()) {
                return ResponseEntity.badRequest().build();
            }
            
            Path documentPath = documentPathOpt.get();
            if (!Files.exists(documentPath) || !Files.isRegularFile(documentPath)) {
                return ResponseEntity.notFound().build();
            }
            
            Resource resource = new FileSystemResource(documentPath);
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                    "attachment; filename=\"" + document + "\"")
                .body(resource);
                
        } catch (Exception e) {
            logger.error("User document access error: {}/{}", userId, document, e);
            return ResponseEntity.badRequest().build();
        }
    }
    
    private String getFileExtension(String filename) {
        int lastDot = filename.lastIndexOf('.');
        return lastDot > 0 ? filename.substring(lastDot) : "";
    }
    
    private String generateUniqueFilename(String originalFilename) {
        String baseName = originalFilename;
        String extension = "";
        
        int lastDot = originalFilename.lastIndexOf('.');
        if (lastDot > 0) {
            baseName = originalFilename.substring(0, lastDot);
            extension = originalFilename.substring(lastDot);
        }
        
        String timestamp = String.valueOf(System.currentTimeMillis());
        String randomId = UUID.randomUUID().toString().substring(0, 8);
        return baseName + "_" + timestamp + "_" + randomId + extension;
    }
    
    private boolean hasAdminRole(Authentication auth) {
        return auth.getAuthorities().stream()
            .anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority()));
    }
}

💡 Why This Fix Works

The vulnerable version directly concatenates user input into file paths without validation, allowing attackers to traverse directories. The secure version uses proper path normalization, validation, and authorization checks.

â„šī¸ Configuration Fix

Configuration changes required - see explanation below.

💡 Explanation

These examples show various techniques attackers use to exploit path traversal vulnerabilities, including encoding bypasses and targeting sensitive system files.

Why it happens

Controllers that directly use request parameters or path variables as file paths without any validation or sanitization.

Root causes

Direct Use of Request Parameters as File Paths

Controllers that directly use request parameters or path variables as file paths without any validation or sanitization.

Preview example – JAVA
@RestController
public class FileController {
    
    private static final String UPLOAD_DIR = "/var/uploads/";
    
    @GetMapping("/files/{filename}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
        // Vulnerable: Direct use of user input as file path
        Path filePath = Paths.get(UPLOAD_DIR + filename);
        Resource resource = new FileSystemResource(filePath);
        
        if (resource.exists()) {
            return ResponseEntity.ok(resource);
        }
        return ResponseEntity.notFound().build();
    }
}

Insufficient Path Validation

Applications that attempt to validate file paths but use inadequate checks that can be bypassed with encoding or alternative traversal sequences.

Preview example – JAVA
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(
        @RequestParam("file") MultipartFile file,
        @RequestParam("directory") String directory) {
    
    // Vulnerable: Insufficient validation
    if (directory.contains("..")) {
        return ResponseEntity.badRequest().body("Invalid directory");
    }
    
    // Still vulnerable to URL encoding, double encoding, etc.
    String targetPath = "/var/uploads/" + directory + "/" + file.getOriginalFilename();
    
    try {
        Files.copy(file.getInputStream(), Paths.get(targetPath));
        return ResponseEntity.ok("File uploaded");
    } catch (IOException e) {
        return ResponseEntity.internalServerError().body("Upload failed");
    }
}

Path Concatenation Without Normalization

Building file paths by concatenating base directories with user input without proper normalization and canonical path resolution.

Preview example – JAVA
@Service
public class DocumentService {
    
    private static final String BASE_DIR = "/app/documents/";
    
    public String readDocument(String userId, String documentName) {
        // Vulnerable: String concatenation without normalization
        String fullPath = BASE_DIR + userId + "/" + documentName;
        
        try {
            return Files.readString(Paths.get(fullPath));
        } catch (IOException e) {
            throw new RuntimeException("Failed to read document", e);
        }
    }
    
    public void saveDocument(String userId, String documentName, String content) {
        // Vulnerable: No validation of path components
        String fullPath = BASE_DIR + userId + "/" + documentName;
        
        try {
            Files.writeString(Paths.get(fullPath), content);
        } catch (IOException e) {
            throw new RuntimeException("Failed to save document", e);
        }
    }
}

Fixes

1

Use Path Normalization and Canonical Path Checking

Always normalize paths and verify that the resolved canonical path stays within the intended base directory.

2

Implement Secure File Path Utility Class

Create a utility class that handles path validation, normalization, and security checks consistently across the application.

3

Implement Allowlist-Based File Access

Use allowlisting to restrict file access to a predefined set of allowed files or patterns.

4

Use Database-Based File Mapping

Instead of using user-provided filenames directly, map files using database IDs or UUIDs to completely avoid path traversal issues.

Detect This Vulnerability in Your Code

Sourcery automatically identifies path traversal from http request data in file access in spring and many other security issues in your codebase.