Servlet Runtime Error Message Containing Sensitive Information

Description

Servlet Runtime Error Message Containing Sensitive Information is a vulnerability where a servlet application produces error messages that reveal sensitive system data to users. When unhandled exceptions occur in servlet code, the runtime environment may display detailed stack traces, file paths, configuration details, and code snippets to end users. This information disclosure helps attackers understand the application architecture, identify specific technologies and versions in use, and discover potential attack vectors.

Risk

Servlet runtime error messages pose significant reconnaissance risks. Stack traces reveal package names, class hierarchies, and method names that expose application internals. File paths in errors show the application's directory structure and installation location. Configuration errors may expose database connection strings, server names, or authentication details. Exception messages may contain user input that was being processed, potentially revealing validation logic or expected data formats. Attackers can deliberately trigger errors by supplying malformed input to systematically map the application's internal structure.

Solution

Implement comprehensive exception handling in all servlet methods. Configure the servlet container to display custom error pages instead of default exception output. Catch all runtime exceptions within servlet code and log detailed information server-side while presenting generic error messages to users. Use web.xml error-page configurations to handle specific exception types and HTTP error codes. Avoid using exception messages or stack traces in user-visible output. Implement a global exception handler filter that intercepts unhandled exceptions before they reach the user.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Error messages may disclose file locations, absolute paths of the web root, application file locations, configuration information, and portions of code that failed.

Example Code

Vulnerable Code

// Vulnerable: Servlet with unhandled exceptions
public class VulnerableServlet extends HttpServlet {

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

        // Vulnerable: NullPointerException if username is null
        String username = request.getParameter("username");
        if (username.length() < 10) {  // NPE here exposes stack trace
            response.getWriter().println("Username too short");
        }

        // Vulnerable: Number format exception exposes details
        String ageStr = request.getParameter("age");
        int age = Integer.parseInt(ageStr);  // Throws on invalid input

        // Vulnerable: File operation error reveals paths
        String configPath = "/etc/app/config.properties";
        FileInputStream fis = new FileInputStream(configPath);  // Reveals path on error

        // Vulnerable: Database error exposes connection details
        String dbUrl = "jdbc:mysql://prod-db:3306/users";
        Connection conn = DriverManager.getConnection(dbUrl, "admin", "password");
        // Connection failure reveals database hostname
    }

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

        // Vulnerable: Array index error reveals application logic
        String[] items = request.getParameterValues("items");
        String first = items[0];  // ArrayIndexOutOfBoundsException

        // Vulnerable: Division by zero reveals processing logic
        int total = Integer.parseInt(request.getParameter("total"));
        int count = Integer.parseInt(request.getParameter("count"));
        int average = total / count;  // ArithmeticException on zero
    }
}

// Example error output visible to user:
// java.lang.NullPointerException
//     at com.example.app.servlet.VulnerableServlet.doPost(VulnerableServlet.java:25)
//     at javax.servlet.http.HttpServlet.service(HttpServlet.java:650)
//     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(...)
// Reveals: package structure, class names, line numbers, server details
// Vulnerable: JSP with inline exception exposure
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%
    // Vulnerable: No exception handling
    String userId = request.getParameter("id");
    int id = Integer.parseInt(userId);

    // Vulnerable: Direct database access with exposed errors
    Class.forName("com.mysql.jdbc.Driver");
    Connection conn = DriverManager.getConnection(
        "jdbc:mysql://db.internal:3306/users", "webapp", "secret123"
    );

    // Error on invalid driver exposes:
    // java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
    // at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    // ...

    // Error on connection failure exposes:
    // com.mysql.jdbc.exceptions.jdbc4.CommunicationsException:
    // Communications link failure to db.internal:3306
%>
<!-- Vulnerable: web.xml without custom error pages -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>com.example.VulnerableServlet</servlet-class>
    </servlet>

    <!-- Vulnerable: No error-page configurations -->
    <!-- Default container error pages will show stack traces -->
</web-app>

Fixed Code

// Fixed: Servlet with comprehensive exception handling
public class SecureServlet extends HttpServlet {
    private static final Logger logger = LoggerFactory.getLogger(SecureServlet.class);

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

        try {
            // Fixed: Null-safe parameter handling
            String username = request.getParameter("username");
            if (username == null || username.isEmpty()) {
                sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
                    "Username is required");
                return;
            }

            if (username.length() < 10) {
                sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
                    "Username must be at least 10 characters");
                return;
            }

            // Fixed: Safe number parsing with validation
            String ageStr = request.getParameter("age");
            int age;
            try {
                age = Integer.parseInt(ageStr);
                if (age < 0 || age > 150) {
                    sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
                        "Invalid age value");
                    return;
                }
            } catch (NumberFormatException e) {
                logger.warn("Invalid age parameter: {}", sanitize(ageStr));
                sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
                    "Age must be a valid number");
                return;
            }

            // Fixed: File operations with generic error
            try {
                processConfigFile();
            } catch (IOException e) {
                logger.error("Configuration file error", e);
                sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "Configuration error. Please contact support.");
                return;
            }

            // Fixed: Database operations with safe error handling
            try {
                processUserData(username, age);
            } catch (SQLException e) {
                logger.error("Database error processing user data", e);
                sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "Unable to process request. Please try again later.");
                return;
            }

            response.getWriter().println("Success");

        } catch (Exception e) {
            // Fixed: Catch-all for unexpected errors
            logger.error("Unexpected error in doPost", e);
            sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                "An unexpected error occurred. Reference: " + generateErrorId());
        }
    }

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

        try {
            // Fixed: Safe array handling
            String[] items = request.getParameterValues("items");
            if (items == null || items.length == 0) {
                sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
                    "At least one item is required");
                return;
            }
            String first = items[0];

            // Fixed: Division with zero check
            int total = parseIntSafe(request.getParameter("total"), 0);
            int count = parseIntSafe(request.getParameter("count"), 0);

            if (count == 0) {
                sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
                    "Count cannot be zero");
                return;
            }

            int average = total / count;
            response.getWriter().println("Average: " + average);

        } catch (Exception e) {
            logger.error("Unexpected error in doGet", e);
            sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                "An error occurred");
        }
    }

    private void sendErrorResponse(HttpServletResponse response, int statusCode,
            String message) throws IOException {
        response.setStatus(statusCode);
        response.setContentType("application/json");
        response.getWriter().println("{\"error\": \"" + escapeJson(message) + "\"}");
    }

    private int parseIntSafe(String value, int defaultValue) {
        try {
            return value != null ? Integer.parseInt(value) : defaultValue;
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    private String sanitize(String input) {
        if (input == null) return "null";
        return input.replaceAll("[^a-zA-Z0-9]", "_").substring(0, Math.min(input.length(), 50));
    }

    private String generateErrorId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    private String escapeJson(String text) {
        return text.replace("\"", "\\\"");
    }
}
// Fixed: Global exception handling filter
public class ExceptionHandlingFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(ExceptionHandlingFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        try {
            chain.doFilter(request, response);
        } catch (Exception e) {
            String errorId = UUID.randomUUID().toString().substring(0, 8);

            // Fixed: Log full details server-side
            logger.error("Unhandled exception [{}]", errorId, e);

            // Fixed: Return generic error to user
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            httpResponse.setContentType("text/html");
            httpResponse.getWriter().println(
                "<html><body>" +
                "<h1>An error occurred</h1>" +
                "<p>Please try again later. Error reference: " + errorId + "</p>" +
                "</body></html>"
            );
        }
    }
}
<!-- Fixed: web.xml with custom error pages -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>SecureServlet</servlet-name>
        <servlet-class>com.example.SecureServlet</servlet-class>
    </servlet>

    <!-- Fixed: Global exception handling filter -->
    <filter>
        <filter-name>ExceptionHandler</filter-name>
        <filter-class>com.example.ExceptionHandlingFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ExceptionHandler</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- Fixed: Custom error pages for HTTP errors -->
    <error-page>
        <error-code>404</error-code>
        <location>/error/404.html</location>
    </error-page>
    <error-page>
        <error-code>500</error-code>
        <location>/error/500.html</location>
    </error-page>

    <!-- Fixed: Custom error pages for exceptions -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/error/generic.html</location>
    </error-page>
    <error-page>
        <exception-type>java.lang.RuntimeException</exception-type>
        <location>/error/generic.html</location>
    </error-page>
    <error-page>
        <exception-type>java.sql.SQLException</exception-type>
        <location>/error/generic.html</location>
    </error-page>
</web-app>

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, servlet error information disclosure is commonly observed in:

  • Misconfigured Apache Tomcat installations
  • Default J2EE application deployments
  • Development mode settings in production

References

  1. MITRE Corporation. "CWE-536: Servlet Runtime Error Message Containing Sensitive Information." https://cwe.mitre.org/data/definitions/536.html
  2. OWASP. "Error Handling Cheat Sheet."
  3. Oracle. "Java Servlet Specification - Error Handling."