Skip to content

CWE-209: Error Message Information Leak - Java

Overview

Error Message Information Leak in Java applications occurs when exception stack traces, SQL error details, or internal system information is exposed to users through HTTP responses, log files, or error pages. Java's comprehensive exception hierarchy and detailed stack traces are invaluable for debugging but dangerous when exposed to untrusted users.

Primary Defence: Return generic error messages to users while logging detailed exceptions server-side, use @ControllerAdvice or exception handlers to centralize error handling, and sanitize all error responses.

Common Vulnerable Patterns

Returning Exception Messages Directly

// VULNERABLE - Exposes database errors and SQL queries
@RestController
public class UserController {

    @GetMapping("/user/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        try {
            User user = userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
            return ResponseEntity.ok(user);
        } catch (Exception e) {
            // Returns: "ORA-00942: table or view does not exist"
            // or "Connection refused: connect"
            return ResponseEntity.status(500)
                .body(new ErrorResponse(e.getMessage()));
        }
    }
}

Why this is vulnerable:

  • Exposes SQL error codes, table names, and column names.
  • Reveals database vendor/version and schema structure.
  • Leaks connection details and internal topology.
  • Aids SQL injection and privilege escalation planning.

Printing Stack Traces to Response

// VULNERABLE - Exposes full stack trace with package structure
@RestController
public class OrderController {

    @PostMapping("/order")
    public ResponseEntity<?> createOrder(@RequestBody Order order) {
        try {
            return ResponseEntity.ok(orderService.process(order));
        } catch (Exception e) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);

            // Exposes: class names, method names, line numbers, file paths
            return ResponseEntity.status(500)
                .body(Map.of(
                    "error", e.getMessage(),
                    "stackTrace", sw.toString()
                ));
        }
    }
}

Why this is vulnerable:

  • Exposes package structure, class names, and line numbers.
  • Reveals internal architecture and execution flow.
  • Leaks third-party libraries and potential versions.
  • Helps attackers map code paths and weak points.

Spring Boot Default Error Handling

// VULNERABLE - Default Spring Boot error page shows too much
// application.properties
server.error.include-exception=true       // Shows exception class
server.error.include-stacktrace=always    // Shows full stack trace
server.error.include-message=always       // Shows exception message
spring.mvc.throw-exception-if-no-handler-found=true

// This exposes detailed error information in JSON responses:
// {
//   "timestamp": "2024-01-15T10:30:00.000+00:00",
//   "status": 500,
//   "error": "Internal Server Error",
//   "exception": "java.sql.SQLException",
//   "message": "ORA-00001: unique constraint violated",
//   "trace": "java.sql.SQLException: ORA-00001...\n\tat com.example..."
// }

Why this is vulnerable:

  • Exposes exception class names and framework internals.
  • Returns full stack traces with file paths and line numbers.
  • Leaks database constraints, validation rules, or SQL fragments.
  • Provides a map of internal structure and error flow.

Servlet Exception Forwarding

// VULNERABLE - Default servlet error handling exposes details
@WebServlet("/process")
public class ProcessServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        try {
            String data = request.getParameter("data");
            processData(data);
            response.getWriter().write("Success");
        } catch (Exception e) {
            // Container's default error page shows exception details
            throw new ServletException(e);
        }
    }
}

Why this is vulnerable:

  • Default container pages expose full stack traces.
  • Reveals servlet paths, request parameters, and server version.
  • Enables mapping of endpoints and internal flow.
  • Helps attackers target specific server versions.

Logging Sensitive Data at DEBUG Level

// VULNERABLE - Logs passwords and tokens
@Service
public class AuthService {

    private static final Logger logger = LoggerFactory.getLogger(AuthService.class);

    public AuthToken login(String username, String password) {
        // Logs plaintext password!
        logger.debug("Login attempt - username: {}, password: {}", username, password);

        User user = userRepository.findByUsername(username);
        if (user != null && passwordEncoder.matches(password, user.getPasswordHash())) {
            AuthToken token = generateToken(user);
            // Logs sensitive token!
            logger.debug("Generated token: {}", token.getValue());
            return token;
        }
        throw new AuthenticationException("Invalid credentials");
    }
}

Why this is vulnerable:

  • Logs plaintext passwords, tokens, and session IDs.
  • Logs may be accessible via aggregation or backups.
  • Over-privileged access exposes sensitive data.
  • Enables account takeover and session hijacking.

Exposing Validation Errors

// VULNERABLE - Reveals field names and internal validation logic
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody UserRegistration registration, 
                                   BindingResult result) {
    if (result.hasErrors()) {
        // Exposes: "passwordHash: must not be null" or 
        // "internalUserId: must match pattern [0-9]{10}"
        List<String> errors = result.getAllErrors().stream()
            .map(ObjectError::toString)
            .collect(Collectors.toList());
        return ResponseEntity.badRequest().body(errors);
    }
    return ResponseEntity.ok(userService.register(registration));
}

Why this is vulnerable:

  • Exposes internal field names and data model structure.
  • Reveals validation patterns and business rules.
  • Discloses internal naming conventions.
  • Helps attackers craft precise probes and enumeration.

Secure Patterns

Spring Boot Global Exception Handler

// SECURE - Generic errors to users, detailed logs server-side
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex, 
                                                                 WebRequest request) {
        // Generate unique error ID for tracking
        String errorId = UUID.randomUUID().toString();

        // Log full details server-side
        logger.error("Error ID {}: {} - Request: {}", 
            errorId, ex.getMessage(), request.getDescription(false), ex);

        // Return generic message with tracking ID
        ErrorResponse error = new ErrorResponse(
            "An error occurred processing your request",
            errorId,
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "Resource not found",
            null,
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
        // Log but don't expose validation details
        logger.warn("Validation error: {}", ex.getMessage());

        ErrorResponse error = new ErrorResponse(
            "Invalid input provided",
            null,
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
}

@Data
@AllArgsConstructor
public class ErrorResponse {
    private String message;
    private String errorId;
    private LocalDateTime timestamp;
}

Why this works:

  • Centralizes exception handling for consistent responses.
  • Logs full details server-side while returning generic messages.
  • Error IDs correlate user reports to server logs safely.
  • Specific handlers keep correct HTTP status codes without leaks.

Secure Spring Boot Configuration

# SECURE - Production application.properties
# Disable detailed error responses
server.error.include-exception=false
server.error.include-stacktrace=never
server.error.include-message=never
server.error.include-binding-errors=never

# Custom error path
server.error.path=/error

# Logging configuration
logging.level.root=INFO
logging.level.com.yourapp=INFO
logging.file.name=/var/log/yourapp/application.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %level - %msg%n
// SECURE - Custom error controller
@Controller
public class CustomErrorController implements ErrorController {

    private static final Logger logger = LoggerFactory.getLogger(CustomErrorController.class);

    @RequestMapping("/error")
    public ResponseEntity<ErrorResponse> handleError(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        Exception exception = (Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

        String errorId = UUID.randomUUID().toString();

        // Log the actual error
        if (exception != null) {
            logger.error("Error ID {}: {}", errorId, exception.getMessage(), exception);
        }

        // Return generic response based on status
        HttpStatus status = HttpStatus.valueOf(statusCode != null ? statusCode : 500);
        String message = status.is4xxClientError() ? 
            "Invalid request" : "An error occurred";

        return new ResponseEntity<>(
            new ErrorResponse(message, errorId, LocalDateTime.now()),
            status
        );
    }
}

Why this works:

  • Disables exception, stack trace, and message exposure in responses.
  • Custom controller returns generic messages by status code.
  • Error IDs provide server-side traceability.
  • Logs stay server-side and outside the web root.

JAX-RS Exception Mappers

// SECURE - JAX-RS global exception handling
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {

    private static final Logger logger = LoggerFactory.getLogger(GenericExceptionMapper.class);

    @Override
    public Response toResponse(Exception exception) {
        String errorId = UUID.randomUUID().toString();

        // Log full exception details
        logger.error("Error ID {}: {}", errorId, exception.getMessage(), exception);

        // Return generic error to client
        ErrorResponse error = new ErrorResponse(
            "An error occurred",
            errorId,
            System.currentTimeMillis()
        );

        return Response
            .status(Response.Status.INTERNAL_SERVER_ERROR)
            .entity(error)
            .build();
    }
}

@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    private static final Logger logger = LoggerFactory.getLogger(ValidationExceptionMapper.class);

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        // Log validation details
        logger.warn("Validation failed: {}", exception.getConstraintViolations());

        // Return generic validation error
        ErrorResponse error = new ErrorResponse(
            "Invalid input",
            null,
            System.currentTimeMillis()
        );

        return Response
            .status(Response.Status.BAD_REQUEST)
            .entity(error)
            .build();
    }
}

Why this works:

  • Global mappers enforce consistent error handling.
  • Full server-side logging with generic client responses.
  • Separate mappers return correct status codes safely.
  • Error IDs link client errors to server logs.

Servlet Error Handling

// SECURE - Servlet with proper error handling
@WebServlet("/secure-process")
public class SecureProcessServlet extends HttpServlet {

    private static final Logger logger = LoggerFactory.getLogger(SecureProcessServlet.class);

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        String errorId = UUID.randomUUID().toString();

        try {
            String data = request.getParameter("data");
            String result = processData(data);

            response.setContentType("application/json");
            response.getWriter().write("{\"status\":\"success\",\"result\":\"" + result + "\"}");

        } catch (Exception e) {
            // Log full error server-side
            logger.error("Error ID {}: Processing failed", errorId, e);

            // Return generic error to client
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.setContentType("application/json");
            response.getWriter().write(String.format(
                "{\"error\":\"An error occurred\",\"errorId\":\"%s\"}", 
                errorId
            ));
        }
    }
}
web.xml configuration for custom error pages
<error-page>
    <error-code>404</error-code>
    <location>/WEB-INF/error-pages/404.jsp</location>
</error-page>
<error-page>
    <error-code>500</error-code>
    <location>/WEB-INF/error-pages/500.jsp</location>
</error-page>
<error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/WEB-INF/error-pages/error.jsp</location>
</error-page>

Why this works:

  • Try/catch prevents container default error pages.
  • Server logs keep full details with an error ID.
  • Custom error pages handle unhandled exceptions safely.
  • /WEB-INF prevents direct access to error views.

Structured Logging with Redaction

// SECURE - Redacting layout to remove sensitive data
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import java.util.regex.Pattern;

public class SensitiveDataRedactingLayout extends PatternLayout {

    private static final Pattern PASSWORD_PATTERN = 
        Pattern.compile("password[\"']?\\s*[:=]\\s*[\"']?([^\"'}\\s]+)", Pattern.CASE_INSENSITIVE);
    private static final Pattern TOKEN_PATTERN = 
        Pattern.compile("token[\"']?\\s*[:=]\\s*[\"']?([^\"'}\\s]+)", Pattern.CASE_INSENSITIVE);
    private static final Pattern CARD_PATTERN = 
        Pattern.compile("\\b\\d{13,19}\\b");
    private static final Pattern EMAIL_PATTERN = 
        Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b");

    @Override
    public String doLayout(ILoggingEvent event) {
        String message = super.doLayout(event);

        message = PASSWORD_PATTERN.matcher(message).replaceAll("password=***REDACTED***");
        message = TOKEN_PATTERN.matcher(message).replaceAll("token=***REDACTED***");
        message = CARD_PATTERN.matcher(message).replaceAll("***CARD***");
        message = EMAIL_PATTERN.matcher(message).replaceAll("***EMAIL***");

        return message;
    }
}
logback.xml configuration
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>/var/log/yourapp/application.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <layout class="com.yourapp.logging.SensitiveDataRedactingLayout"/>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE"/>
    </root>
</configuration>

Why this works:

  • Layout rewrites the final log line before output.
  • Regex patterns redact common secrets consistently.
  • Works across all log statements without app changes.
  • Keeps useful context while removing sensitive values.

Validation Error Sanitization

// SECURE - Sanitized validation error responses
@RestControllerAdvice
public class ValidationExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(ValidationExceptionHandler.class);

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {

        // Log detailed validation errors server-side
        List<String> detailedErrors = ex.getBindingResult()
            .getAllErrors()
            .stream()
            .map(ObjectError::toString)
            .collect(Collectors.toList());
        logger.warn("Validation failed: {}", detailedErrors);

        // Return generic error to client
        ErrorResponse error = new ErrorResponse(
            "Invalid input provided",
            null,
            LocalDateTime.now()
        );

        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleConstraintViolation(
            ConstraintViolationException ex) {

        // Log details
        logger.warn("Constraint violation: {}", ex.getConstraintViolations());

        // Generic response
        ErrorResponse error = new ErrorResponse(
            "Request validation failed",
            null,
            LocalDateTime.now()
        );

        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
}

Why this works:

  • Logs detailed validation errors server-side only.
  • Returns generic messages to clients.
  • Prevents exposure of internal field names and rules.
  • Global handlers keep behavior consistent.

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Additional Resources