Introduction

Welcome to the lesson on directory listing, a specific type of security misconfiguration that can pose significant risks to web applications. In previous lessons, we explored the concept of security misconfiguration and the dangers of default credentials. Now, we'll focus on directory listing, which can inadvertently expose sensitive files and data to unauthorized users. Understanding and mitigating this vulnerability is crucial for maintaining the security of your web applications. Let's dive in! 🚀

What is Directory Listing?

Directory listing is a feature of web servers that allows users to view the contents of a directory when no specific file is requested. This feature was originally designed to make file sharing and navigation easier, particularly in development environments or for simple file-sharing services. For example, when hosting documentation or downloadable resources, directory listing can provide a simple way for users to browse and access files.

However, this convenience comes with significant security risks when implemented in production environments. When directory listing is enabled, anyone can access a list of files in a directory, potentially revealing sensitive information like configuration files, credentials, or other private data.

It's important to note that directory listing is typically disabled by default in Spring Boot and Tomcat. The vulnerability arises when:

  • Developers explicitly enable it through servlet container configuration
  • Static resource serving is misconfigured, exposing all files in a directory
  • Poor file management practices place sensitive files in publicly accessible locations

Even without directory listing enabled, improperly configured static resources can allow direct access to sensitive files if attackers know or guess the filenames. Let's see how this vulnerability manifests in actual code and explore various ways to protect against it.

Vulnerable Code Example

Consider a scenario where you're building a file-sharing application that needs to serve uploaded files to users. You might be tempted to configure Spring Boot to serve files from an uploads directory.

Spring Boot provides built-in support for serving static resources, and while directory listing is disabled by default, misconfiguring static resources can still expose sensitive files. Here's an example that demonstrates this vulnerability in application.yml:

server:
  port: 3000

spring:
  datasource:
    url: jdbc:sqlite:database.sqlite
    driver-class-name: org.sqlite.JDBC
  jpa:
    database-platform: org.hibernate.community.dialect.SQLiteDialect
    hibernate:
      ddl-auto: update
    show-sql: false
  web:
    # Vulnerable: Serving uploads directory as static resources
    # This exposes all files if an attacker knows or guesses filenames
    resources:
      static-locations: file:uploads/

app:
  jwt:
    secret: jwt-secret-key
  uploads:
    directory: uploads

Additionally, here's a vulnerable Spring configuration class:

package com.codesignal.pastebin.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // Vulnerable: Serving entire uploads directory as static resources
        // This allows unrestricted access to all files in the uploads folder
        // Note: This doesn't enable directory listing by default, but does allow
        // direct access to any file if the attacker knows or guesses the filename
        registry.addResourceHandler("/uploads/**")
                .addResourceLocations("file:uploads/");
    }
}

While this configuration doesn't enable directory listing by default (which would require additional servlet container configuration), it still creates a significant vulnerability: any file in the uploads directory becomes accessible if an attacker knows or guesses its name. This is particularly dangerous when developers store backup files, configuration files, or files with predictable names like .env or config.yml. Let's examine how an attacker might exploit this configuration.

Exploiting the Vulnerability

An attacker can exploit this vulnerability by using simple commands to access sensitive files. Here's how it might be done using curl:

# Get sensitive data from the uploads directory
curl http://localhost:3000/uploads/sensitive-data.txt

# Access potentially sensitive dotfiles
curl http://localhost:3000/uploads/.env

# Try common configuration file names
curl http://localhost:3000/uploads/config.yml
curl http://localhost:3000/uploads/database.sqlite

These commands use curl to access files within the uploads directory. Even without directory listing enabled, an attacker can retrieve sensitive files like sensitive-data.txt or .env if they guess or discover the filenames through other means (like version control leaks or error messages). The vulnerability becomes even more severe if directory listing is explicitly enabled through servlet container configuration, as this would allow attackers to browse and discover all available files. Now, let's explore different strategies to mitigate this vulnerability.

Removing Directory Listing Middleware

One mitigation strategy is to ensure that files in the uploads directory are not served as static resources at all. This is the most fundamental security measure:

server:
  port: 3000

spring:
  datasource:
    url: jdbc:sqlite:database.sqlite
    driver-class-name: org.sqlite.JDBC
  jpa:
    database-platform: org.hibernate.community.dialect.SQLiteDialect
    hibernate:
      ddl-auto: update
    show-sql: false
  # Strategy 1: Remove vulnerable static resource configuration
  # Do not enable: spring.web.resources.static-locations

app:
  jwt:
    secret: jwt-secret-key
  uploads:
    directory: uploads

Additionally, remove or secure the vulnerable WebMvcConfigurer:

package com.codesignal.pastebin.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // Strategy 1: Remove the vulnerable resource handler completely
        // The following configuration is removed or commented out:
        // registry.addResourceHandler("/uploads/**")
        //         .addResourceLocations("file:uploads/");
        
        // Only serve necessary frontend assets
        registry.addResourceHandler("/assets/**")
                .addResourceLocations(
                        "file:frontend/dist/assets/",
                        "file:/app/frontend/dist/assets/");
    }
}

By removing the static resource handler for the uploads directory, you prevent automatic serving of all files in that directory, which eliminates the possibility of both directory listing and unauthorized file access. While this is a good start, you might want to consider additional security measures for controlled file access.

Serving Specific Files Only

Another strategy is to explicitly control which files can be accessed. This approach provides fine-grained control over file access using a rest controller:

package com.codesignal.pastebin.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/uploads")
public class UploadsController {
    
    @Value("${app.uploads.directory:uploads}")
    private String uploadsDirectory;
    
    // Strategy 2: Serve only explicitly allowed files
    // Note: In a real application with user-uploaded files, you would:
    // - Validate files against a database of uploaded file records
    // - Check user permissions before serving files
    // - Use authentication to ensure only authorized users access files
    private final List<String> allowedFiles = Arrays.asList(
        "allowed-file.txt",
        "public-document.pdf"
    );
    
    @GetMapping("/{filename}")
    public ResponseEntity<Resource> getFile(@PathVariable String filename) {
        // Check if the file is in the allowed list
        if (!allowedFiles.contains(filename)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
        
        File file = new File(uploadsDirectory, filename);
        
        if (!file.exists() || !file.isFile()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }
        
        Resource resource = new FileSystemResource(file);
        return ResponseEntity.ok()
                .contentType(MediaType.TEXT_PLAIN)
                .body(resource);
    }
}

This change ensures that only explicitly allowed files are accessible through the /uploads/{filename} endpoint, reducing the risk of exposing sensitive data. This approach is particularly useful when you need to maintain strict control over file access. For applications with dynamic uploads, you would replace the hardcoded list with database lookups and authentication checks.

Adding Error Response for Directory Access

Strategy 3 builds on Strategy 2 by adding explicit handling for directory access attempts. While Strategy 2 controls individual file access, Strategy 3 adds clear messaging when someone tries to browse the directory itself:

package com.codesignal.pastebin.controller;

import com.codesignal.pastebin.util.ErrorResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/uploads")
public class UploadsController {
    
    @Value("${app.uploads.directory:uploads}")
    private String uploadsDirectory;
    
    private final List<String> allowedFiles = Arrays.asList(
        "allowed-file.txt",
        "public-document.pdf"
    );
    
    @GetMapping("/{filename}")
    public ResponseEntity<?> getFile(@PathVariable String filename) {
        if (!allowedFiles.contains(filename)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                    .body(new ErrorResponse("Access to this file is forbidden"));
        }
        
        File file = new File(uploadsDirectory, filename);
        
        if (!file.exists() || !file.isFile()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body(new ErrorResponse("File not found"));
        }
        
        Resource resource = new FileSystemResource(file);
        return ResponseEntity.ok()
                .contentType(MediaType.TEXT_PLAIN)
                .body(resource);
    }
    
    // Strategy 3: Add explicit error response for directory access attempts
    // This prevents information leakage and clearly communicates the policy
    @GetMapping("/")
    public ResponseEntity<ErrorResponse> handleDirectoryAccess() {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(new ErrorResponse("Directory listing is forbidden"));
    }
}

This enhanced UploadsController now provides consistent error responses using the ErrorResponse class for all error cases, and explicitly handles directory access attempts. The combination of controlled file access (Strategy 2) and clear directory access denial (Strategy 3) creates a robust defense against both unauthorized file access and information leakage. These strategies can be combined to create a comprehensive security solution.

Conclusion and Next Steps

In this lesson, we explored how misconfigured static resources can expose sensitive files, identified the risks of both unauthorized file access and directory listing, and learned how to mitigate these vulnerabilities by implementing various security strategies in Spring Boot. As you move forward, practice these techniques in the exercises that follow to reinforce your understanding. In the practice exercises that follow, you'll apply these concepts. Then, in the next unit, we'll continue to build on these security concepts to further enhance your web application security skills. Keep up the great work! 🌟

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal