Java Runtime Error Message Containing Sensitive Information

Description

Java Runtime Error Message Containing Sensitive Information is a vulnerability where Java applications expose sensitive data through error messages generated during runtime exceptions. When developers fail to properly handle exceptions, detailed error information—including file paths, system configuration, database details, or user credentials—may be displayed to users or logged insecurely. This information disclosure enables attackers to gain unauthorized access to the system by understanding its internal workings and identifying exploitable weaknesses.

Risk

Java runtime error messages can reveal critical system information to attackers. Exception stack traces expose class names, method signatures, and line numbers that reveal application architecture. Error messages may contain user input that was being processed, such as failed login credentials appearing in authentication exceptions. Database exceptions may reveal connection strings, query syntax, or table structures. File-related exceptions expose directory paths and file system layout. ClassNotFoundException errors reveal expected library names and versions. OutOfMemoryError details may indicate resource constraints that can be exploited for denial of service attacks.

Solution

Implement comprehensive exception handling that captures detailed information for logging while presenting sanitized messages to users. Never include sensitive data like passwords, credentials, or personal information in exception messages. Use logging frameworks to record stack traces and error details server-side. Create custom exception classes that separate internal error details from user-facing messages. Configure application frameworks to disable verbose error output in production. Implement global exception handlers that ensure no unhandled exceptions reach users. Review and sanitize all error messages before display.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Sensitive information becomes accessible to unauthorized parties through error output, including file paths, configuration details, and potentially credentials.

Example Code

Vulnerable Code

// Vulnerable: File operation error reveals directory structure
public class VulnerableFileReader {

    public String readConfig(String filename) {
        try {
            FileReader reader = new FileReader("/opt/app/config/" + filename);
            BufferedReader br = new BufferedReader(reader);
            return br.readLine();
        } catch (FileNotFoundException e) {
            // Vulnerable: Error message reveals full file path
            throw new RuntimeException("Error: " + e.getMessage());
            // Output: Error: /opt/app/config/database.properties (No such file or directory)
        } catch (IOException e) {
            throw new RuntimeException("Read error: " + e.toString());
        }
    }
}

// Vulnerable: Login exception reveals password
public class VulnerableAuthService {

    public void authenticate(String username, String password) throws AuthException {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            // Vulnerable: Reveals valid/invalid username
            throw new AuthException("User not found: " + username);
        }

        if (!passwordEncoder.matches(password, user.getPasswordHash())) {
            // Vulnerable: Password included in error message!
            throw new AuthException("Invalid password '" + password +
                "' for user '" + username + "'");
            // This error might be logged or displayed, exposing the password
        }
    }
}

// Vulnerable: Database exception reveals connection details
public class VulnerableDatabaseService {

    public Connection getConnection() {
        try {
            String url = "jdbc:mysql://db.internal.company.com:3306/production";
            String user = "app_user";
            String password = "s3cr3t_p4ssw0rd";

            return DriverManager.getConnection(url, user, password);
        } catch (SQLException e) {
            // Vulnerable: Full exception reveals connection string
            throw new RuntimeException("Database connection failed: " + e.toString());
            // Output: Database connection failed: com.mysql.jdbc.exceptions.jdbc4.
            // CommunicationsException: Communications link failure...
            // jdbc:mysql://db.internal.company.com:3306/production
        }
    }

    public void executeQuery(String userInput) {
        try {
            Statement stmt = connection.createStatement();
            // Vulnerable: SQL error reveals query structure
            stmt.executeQuery("SELECT * FROM users WHERE name = '" + userInput + "'");
        } catch (SQLException e) {
            // Vulnerable: Shows the SQL query in error
            throw new RuntimeException("Query failed: " + e.getMessage());
            // Output: Query failed: You have an error in your SQL syntax; check the manual...
            // near 'SELECT * FROM users WHERE name = 'malicious input''
        }
    }
}

// Vulnerable: Class loading error reveals expected classes
public class VulnerablePluginLoader {

    public void loadPlugin(String className) {
        try {
            Class<?> pluginClass = Class.forName(className);
            Object plugin = pluginClass.getDeclaredConstructor().newInstance();
        } catch (ClassNotFoundException e) {
            // Vulnerable: Reveals class loading expectations
            throw new RuntimeException("Plugin class not found: " + e.getMessage());
            // Output: Plugin class not found: com.company.plugins.AdminPlugin
        } catch (Exception e) {
            // Vulnerable: Shows instantiation errors with internal details
            throw new RuntimeException("Plugin loading failed: " + e.toString());
        }
    }
}
// Vulnerable: Web service with error exposure
@RestController
public class VulnerableController {

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable String id) {
        try {
            int userId = Integer.parseInt(id);
            return userService.findById(userId);
        } catch (NumberFormatException e) {
            // Vulnerable: Stack trace in response
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Invalid ID: " + e.toString());
        } catch (Exception e) {
            // Vulnerable: Full exception details exposed
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
                "Error: " + e.getMessage() + "\nStack: " + Arrays.toString(e.getStackTrace()));
        }
    }

    @PostMapping("/process")
    public void processData(@RequestBody String data) {
        try {
            JSONObject json = new JSONObject(data);
            // Process...
        } catch (JSONException e) {
            // Vulnerable: Shows parsing details and data
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "JSON parsing failed at position " + e.getMessage() + " in: " + data);
        }
    }
}

Fixed Code

// Fixed: Secure file reading with sanitized errors
public class SecureFileReader {
    private static final Logger logger = LoggerFactory.getLogger(SecureFileReader.class);
    private static final String CONFIG_DIR = "/opt/app/config/";

    public String readConfig(String filename) throws ConfigException {
        // Fixed: Validate filename to prevent path traversal
        if (filename == null || filename.contains("..") || filename.contains("/")) {
            throw new ConfigException("Invalid filename");
        }

        String fullPath = CONFIG_DIR + filename;

        try {
            FileReader reader = new FileReader(fullPath);
            BufferedReader br = new BufferedReader(reader);
            return br.readLine();
        } catch (FileNotFoundException e) {
            // Fixed: Log details, return generic message
            logger.error("Configuration file not found: {}", filename, e);
            throw new ConfigException("Configuration file not available");
        } catch (IOException e) {
            logger.error("Error reading configuration file: {}", filename, e);
            throw new ConfigException("Unable to read configuration");
        }
    }
}

// Fixed: Custom exception with separate internal/external messages
public class ConfigException extends Exception {
    private final String internalMessage;

    public ConfigException(String userMessage) {
        super(userMessage);
        this.internalMessage = userMessage;
    }

    public ConfigException(String userMessage, String internalMessage) {
        super(userMessage);
        this.internalMessage = internalMessage;
    }

    public String getInternalMessage() {
        return internalMessage;
    }
}
// Fixed: Secure authentication without credential exposure
public class SecureAuthService {
    private static final Logger logger = LoggerFactory.getLogger(SecureAuthService.class);

    public AuthResult authenticate(String username, String password) {
        // Fixed: Log attempt without credentials
        logger.info("Authentication attempt for user: {}", sanitizeForLog(username));

        User user = userRepository.findByUsername(username);

        if (user == null) {
            // Fixed: Generic message, don't reveal if user exists
            logger.warn("Failed authentication: user not found - {}", sanitizeForLog(username));
            return AuthResult.failure("Invalid username or password");
        }

        if (!passwordEncoder.matches(password, user.getPasswordHash())) {
            // Fixed: Never log or expose the password
            logger.warn("Failed authentication: invalid password for user - {}",
                sanitizeForLog(username));
            return AuthResult.failure("Invalid username or password");
        }

        logger.info("Successful authentication for user: {}", sanitizeForLog(username));
        return AuthResult.success(user);
    }

    private String sanitizeForLog(String input) {
        if (input == null) return "[null]";
        // Remove potentially dangerous characters for log injection
        return input.replaceAll("[\\r\\n\\t]", "_").substring(0, Math.min(input.length(), 50));
    }
}

// Fixed: Result object instead of exception-based flow
public class AuthResult {
    private final boolean success;
    private final String message;
    private final User user;

    private AuthResult(boolean success, String message, User user) {
        this.success = success;
        this.message = message;
        this.user = user;
    }

    public static AuthResult success(User user) {
        return new AuthResult(true, null, user);
    }

    public static AuthResult failure(String message) {
        return new AuthResult(false, message, null);
    }

    // Getters...
}
// Fixed: Secure database service with error handling
public class SecureDatabaseService {
    private static final Logger logger = LoggerFactory.getLogger(SecureDatabaseService.class);
    private final DataSource dataSource;

    public SecureDatabaseService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Connection getConnection() throws DatabaseException {
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            // Fixed: Log full details, return generic message
            logger.error("Database connection failed", e);
            throw new DatabaseException("Unable to connect to database. Please try again later.");
        }
    }

    public List<User> findUsersByName(String name) throws DatabaseException {
        // Fixed: Use parameterized queries
        String sql = "SELECT * FROM users WHERE name = ?";

        try (Connection conn = getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {

            stmt.setString(1, name);
            ResultSet rs = stmt.executeQuery();

            List<User> users = new ArrayList<>();
            while (rs.next()) {
                users.add(mapUser(rs));
            }
            return users;

        } catch (SQLException e) {
            // Fixed: Log details without exposing query structure
            String errorId = generateErrorId();
            logger.error("Database query failed [{}]", errorId, e);
            throw new DatabaseException(
                "Unable to retrieve user data. Error reference: " + errorId);
        }
    }

    private String generateErrorId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }
}
// Fixed: Secure REST controller with proper error handling
@RestController
@ControllerAdvice
public class SecureController {
    private static final Logger logger = LoggerFactory.getLogger(SecureController.class);

    @GetMapping("/user/{id}")
    public ResponseEntity<User> getUser(@PathVariable String id) {
        // Fixed: Validate input
        if (!id.matches("\\d+")) {
            return ResponseEntity.badRequest().build();
        }

        int userId = Integer.parseInt(id);
        User user = userService.findById(userId);

        if (user == null) {
            return ResponseEntity.notFound().build();
        }

        return ResponseEntity.ok(user);
    }

    @PostMapping("/process")
    public ResponseEntity<ProcessResult> processData(@RequestBody String data) {
        try {
            JSONObject json = new JSONObject(data);
            ProcessResult result = processor.process(json);
            return ResponseEntity.ok(result);
        } catch (JSONException e) {
            // Fixed: Generic error, no data exposure
            logger.warn("Invalid JSON received", e);
            return ResponseEntity.badRequest()
                .body(new ProcessResult(false, "Invalid JSON format"));
        }
    }

    // Fixed: Global exception handler
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        String errorId = UUID.randomUUID().toString().substring(0, 8);
        logger.error("Unhandled exception [{}]", errorId, e);

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse(
                "An error occurred. Reference: " + errorId,
                errorId
            ));
    }
}

// Fixed: Error response class
public class ErrorResponse {
    private final String message;
    private final String errorId;

    public ErrorResponse(String message, String errorId) {
        this.message = message;
        this.errorId = errorId;
    }

    // Getters...
}

CVE Examples

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

  • Spring Boot applications with verbose error handling enabled
  • Misconfigured Java web applications
  • Debug mode configurations in production

References

  1. MITRE Corporation. "CWE-537: Java Runtime Error Message Containing Sensitive Information." https://cwe.mitre.org/data/definitions/537.html
  2. OWASP. "Error Handling Cheat Sheet."
  3. CERT. "ERR01-J. Do not allow exceptions to expose sensitive information."