Introduction

Welcome to the lesson on detailed error messages and their security implications! In this lesson, we'll explore how detailed error messages can inadvertently expose sensitive information about your web application's internal workings. This is a crucial aspect of security misconfiguration, which we've been discussing in previous lessons.

By understanding the risks associated with detailed error messages, you'll be better equipped to secure your applications and protect them from potential attacks. Let's dive in! 🚀

Understanding Detailed Error Messages

Detailed error messages are responses generated by a server when something goes wrong. They often contain information intended to help developers debug issues. However, these messages can also reveal sensitive details about the server's internal structure, such as stack traces, server paths, and even database queries. While this information is valuable during development, exposing it in production can provide attackers with insights they shouldn't have.

Let's see how this vulnerability manifests in practice.

The Vulnerable Code

Here's an error handler that demonstrates how detailed error messages can lead to security issues:

package com.codesignal.pastebin.config;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Vulnerable: Developer left debug information enabled
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(
            Exception ex, 
            HttpServletRequest request) {
        
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("error", ex.getMessage());
        response.put("stack", getStackTrace(ex));
        
        Map<String, Object> debug = new LinkedHashMap<>();
        debug.put("route", request.getRequestURI());
        debug.put("method", request.getMethod());
        debug.put("headers", getHeaders(request));
        debug.put("query", request.getQueryString());
        debug.put("timestamp", Instant.now().toString());
        
        response.put("debug", debug);
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .contentType(MediaType.APPLICATION_JSON)
                .body(response);
    }
    
    private String getStackTrace(Exception ex) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        ex.printStackTrace(pw);
        return sw.toString();
    }
    
    private Map<String, String> getHeaders(HttpServletRequest request) {
        return Collections.list(request.getHeaderNames())
                .stream()
                .collect(Collectors.toMap(
                        headerName -> headerName,
                        request::getHeader
                ));
    }
}

This class uses Spring Boot's @ControllerAdvice annotation, which automatically registers it as a global exception handler for all controllers in your application. The @ExceptionHandler method catches all exceptions and sends a detailed JSON response to the client, including the error message, stack trace, and request details. While this information is helpful for debugging, it can also expose sensitive data to potential attackers.

Let's see how an attacker might exploit this.

Exploiting the Vulnerability

An attacker can exploit this vulnerability by sending requests to invalid endpoints and extracting sensitive information from the error messages.

Here's an example:

$ curl http://localhost:3000/api/nonexistent
{
  "error": "ENOENT: no such file or directory, stat '/usercode/FILESYSTEM/learn_pastebin-java/frontend/dist/index.html'",
  "stack": "java.io.FileNotFoundException: ENOENT: no such file or directory, stat '/usercode/FILESYSTEM/learn_pastebin-java/frontend/dist/index.html'\n\tat com.codesignal.pastebin.controller.ApiErrorController.handleNonExistentApiRoute(ApiErrorController.java:14)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:580)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255)",
  "debug": {
    "route": "/api/nonexistent",
    "method": "GET",
    "headers": {
      "accept": "*/*",
      "user-agent": "curl/7.81.0",
      "host": "localhost:3000",
      "connection": "close"
    },
    "query": null,
    "timestamp": "2025-03-19T11:54:33.656Z"
  }
}

This error response reveals several pieces of sensitive information:

  1. The complete file system path, revealing the application's directory structure.
  2. The technology stack (Spring Boot/Java).
  3. Internal routing information and Spring Boot's internal class structure.
  4. Server configuration details.

An attacker could use this information to plan more targeted attacks or exploit specific vulnerabilities.

Let's look at how to implement secure error handling instead.

Secure Error Logging

To protect your application, it's crucial to implement secure error logging practices. Here's how to properly log errors while maintaining security:

package com.codesignal.pastebin.config;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

@ControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Set<String> SENSITIVE_HEADERS = Set.of(
        "authorization", "cookie", "x-api-key", "x-auth-token"
    );

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(
            Exception ex, 
            HttpServletRequest request) {
        
        // Log detailed error information for debugging
        Map<String, Object> logData = new LinkedHashMap<>();
        logData.put("error", ex.getMessage());
        logData.put("stack", getStackTrace(ex));
        
        Map<String, Object> debug = new LinkedHashMap<>();
        debug.put("route", request.getRequestURI());
        debug.put("method", request.getMethod());
        debug.put("headers", getSanitizedHeaders(request));
        debug.put("query", request.getQueryString());
        debug.put("timestamp", Instant.now().toString());
        
        logData.put("debug", debug);
        
        System.err.println(logData);
        
        // Send generic error response to client
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("error", "An unexpected error occurred");
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .contentType(MediaType.APPLICATION_JSON)
                .body(response);
    }
    
    private String getStackTrace(Exception ex) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        ex.printStackTrace(pw);
        return sw.toString();
    }
    
    private Map<String, String> getSanitizedHeaders(HttpServletRequest request) {
        return Collections.list(request.getHeaderNames())
                .stream()
                .collect(Collectors.toMap(
                        headerName -> headerName,
                        headerName -> {
                            if (SENSITIVE_HEADERS.contains(headerName.toLowerCase())) {
                                return "[REDACTED]";
                            }
                            return request.getHeader(headerName);
                        }
                ));
    }
}

This approach ensures that detailed error information is logged internally using System.err.println(), allowing developers to debug issues without exposing sensitive data to clients. The error details are printed to the standard error stream where they can be captured by logging systems.

Critical Security Note: Notice how we sanitize headers before logging using the getSanitizedHeaders() method. Headers like Authorization (which may contain Bearer tokens), Cookie, and other credential-carrying headers should never be logged in production, as they could expose authentication tokens, session IDs, and other sensitive credentials. The SENSITIVE_HEADERS set defines which headers to redact, replacing their values with [REDACTED]. Now, let's see how to handle the client response securely.

Environment-Based Error Responses

Another approach is to implement environment-based error responses that provide appropriate information based on the application's environment:

package com.codesignal.pastebin.config;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @Value("${spring.profiles.active:production}")
    private String activeProfile;

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(
            Exception ex, 
            HttpServletRequest request) {
        
        // Log detailed error information for debugging
        Map<String, Object> logData = new LinkedHashMap<>();
        logData.put("error", ex.getMessage());
        logData.put("stack", getStackTrace(ex));
        
        Map<String, Object> debug = new LinkedHashMap<>();
        debug.put("route", request.getRequestURI());
        debug.put("method", request.getMethod());
        debug.put("headers", getHeaders(request));
        debug.put("query", request.getQueryString());
        debug.put("timestamp", Instant.now().toString());
        
        logData.put("debug", debug);
        
        System.err.println(logData);
        
        Map<String, Object> response = new LinkedHashMap<>();
        
        boolean isProduction = "production".equalsIgnoreCase(activeProfile);
        
        if (isProduction) {
            response.put("error", "An unexpected error occurred");
        } else {
            response.put("error", ex.getMessage());
        }
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .contentType(MediaType.APPLICATION_JSON)
                .body(response);
    }
    
    private String getStackTrace(Exception ex) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        ex.printStackTrace(pw);
        return sw.toString();
    }
    
    private Map<String, String> getHeaders(HttpServletRequest request) {
        return Collections.list(request.getHeaderNames())
                .stream()
                .collect(Collectors.toMap(
                        headerName -> headerName,
                        request::getHeader
                ));
    }
}

In production, we return a generic error message to the client, while in development, we provide the error message without sensitive debug information. This approach balances the need for debugging with security considerations. The @Value annotation injects the active Spring profile from your application properties, defaulting to production if none is specified.

Conclusion and Next Steps

In this lesson, we've explored the risks associated with exposing detailed error messages and how attackers can exploit them. By identifying vulnerable code and implementing secure error handling practices, you can protect your applications from potential attacks. As you move on to the practice exercises, remember to apply these concepts to enhance your application's web application security.

Congratulations on completing this course! You've now learned essential security misconfiguration mitigation techniques that will help protect your applications in production! 🎉

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