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
| Impact | Details |
|---|---|
| Confidentiality | Scope: 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
- MITRE Corporation. "CWE-537: Java Runtime Error Message Containing Sensitive Information." https://cwe.mitre.org/data/definitions/537.html
- OWASP. "Error Handling Cheat Sheet."
- CERT. "ERR01-J. Do not allow exceptions to expose sensitive information."