Java-Laufzeitfehlermeldung mit sensiblen Informationen
Beschreibung
Java-Laufzeitfehlermeldung mit sensiblen Informationen ist eine Schwachstelle, bei der Java-Anwendungen sensible Daten durch Fehlermeldungen offenlegen, die während Laufzeitausnahmen generiert werden. Wenn Entwickler Ausnahmen nicht ordnungsgemäß behandeln, können detaillierte Fehlerinformationen—einschließlich Dateipfade, Systemkonfiguration, Datenbankdetails oder Benutzeranmeldedaten—Benutzern angezeigt oder unsicher protokolliert werden. Diese Informationsoffenlegung ermöglicht Angreifern unbefugten Zugriff auf das System durch Verständnis seiner internen Funktionsweise und Identifizierung ausnutzbarer Schwachstellen.
Risiko
Java-Laufzeitfehlermeldungen können kritische Systeminformationen an Angreifer offenbaren. Ausnahme-Stack-Traces legen Klassennamen, Methodensignaturen und Zeilennummern offen, die die Anwendungsarchitektur enthüllen. Fehlermeldungen können Benutzereingaben enthalten, die verarbeitet wurden, wie fehlgeschlagene Anmeldedaten in Authentifizierungsausnahmen. Datenbankausnahmen können Verbindungsstrings, Abfragesyntax oder Tabellenstrukturen offenlegen. Dateibezogene Ausnahmen legen Verzeichnispfade und Dateisystemlayout offen. ClassNotFoundException-Fehler offenbaren erwartete Bibliotheksnamen und -versionen. OutOfMemoryError-Details können Ressourcenbeschränkungen anzeigen, die für Denial-of-Service-Angriffe ausgenutzt werden können.
Lösung
Implementieren Sie umfassende Ausnahmebehandlung, die detaillierte Informationen für die Protokollierung erfasst, während Benutzern bereinigte Meldungen präsentiert werden. Fügen Sie niemals sensible Daten wie Passwörter, Anmeldedaten oder persönliche Informationen in Ausnahmemeldungen ein. Verwenden Sie Logging-Frameworks, um Stack-Traces und Fehlerdetails serverseitig aufzuzeichnen. Erstellen Sie benutzerdefinierte Ausnahmeklassen, die interne Fehlerdetails von benutzerorientierten Meldungen trennen. Konfigurieren Sie Anwendungsframeworks, um ausführliche Fehlerausgabe in Produktion zu deaktivieren. Implementieren Sie globale Ausnahmehandler, die sicherstellen, dass keine unbehandelten Ausnahmen Benutzer erreichen. Überprüfen und bereinigen Sie alle Fehlermeldungen vor der Anzeige.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Vertraulichkeit Anwendungsdaten lesen - Sensible Informationen werden durch Fehlerausgabe für unbefugte Parteien zugänglich, einschließlich Dateipfade, Konfigurationsdetails und potenziell Anmeldedaten. |
Beispielcode + Lösungscode
Verwundbarer Code
// Verwundbar: Dateioperationsfehler offenbart Verzeichnisstruktur
public class VerwundbarerFileReader {
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) {
// Verwundbar: Fehlermeldung offenbart vollständigen Dateipfad
throw new RuntimeException("Fehler: " + e.getMessage());
// Ausgabe: Fehler: /opt/app/config/database.properties (No such file or directory)
} catch (IOException e) {
throw new RuntimeException("Lesefehler: " + e.toString());
}
}
}
// Verwundbar: Login-Ausnahme offenbart Passwort
public class VerwundbarerAuthService {
public void authenticate(String username, String password) throws AuthException {
User user = userRepository.findByUsername(username);
if (user == null) {
// Verwundbar: Offenbart gültigen/ungültigen Benutzernamen
throw new AuthException("Benutzer nicht gefunden: " + username);
}
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
// Verwundbar: Passwort in Fehlermeldung enthalten!
throw new AuthException("Ungültiges Passwort '" + password +
"' für Benutzer '" + username + "'");
// Dieser Fehler könnte protokolliert oder angezeigt werden, Passwort offengelegt
}
}
}
// Verwundbar: Datenbankausnahme offenbart Verbindungsdetails
public class VerwundbarerDatabaseService {
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) {
// Verwundbar: Vollständige Ausnahme offenbart Verbindungsstring
throw new RuntimeException("Datenbankverbindung fehlgeschlagen: " + e.toString());
// Ausgabe: Datenbankverbindung fehlgeschlagen: 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();
// Verwundbar: SQL-Fehler offenbart Abfragestruktur
stmt.executeQuery("SELECT * FROM users WHERE name = '" + userInput + "'");
} catch (SQLException e) {
// Verwundbar: Zeigt SQL-Abfrage im Fehler
throw new RuntimeException("Abfrage fehlgeschlagen: " + e.getMessage());
// Ausgabe: Abfrage fehlgeschlagen: You have an error in your SQL syntax; check the manual...
// near 'SELECT * FROM users WHERE name = 'malicious input''
}
}
}
// Verwundbar: Klassenlladefehler offenbart erwartete Klassen
public class VerwundbarerPluginLoader {
public void loadPlugin(String className) {
try {
Class<?> pluginClass = Class.forName(className);
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
// Verwundbar: Offenbart Klassenlade-Erwartungen
throw new RuntimeException("Plugin-Klasse nicht gefunden: " + e.getMessage());
// Ausgabe: Plugin-Klasse nicht gefunden: com.company.plugins.AdminPlugin
} catch (Exception e) {
// Verwundbar: Zeigt Instanziierungsfehler mit internen Details
throw new RuntimeException("Plugin-Laden fehlgeschlagen: " + e.toString());
}
}
}
// Verwundbar: Webservice mit Fehleroffenlegung
@RestController
public class VerwundbarerController {
@GetMapping("/user/{id}")
public User getUser(@PathVariable String id) {
try {
int userId = Integer.parseInt(id);
return userService.findById(userId);
} catch (NumberFormatException e) {
// Verwundbar: Stack-Trace in Antwort
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Ungültige ID: " + e.toString());
} catch (Exception e) {
// Verwundbar: Vollständige Ausnahmedetails offengelegt
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
"Fehler: " + e.getMessage() + "\nStack: " + Arrays.toString(e.getStackTrace()));
}
}
@PostMapping("/process")
public void processData(@RequestBody String data) {
try {
JSONObject json = new JSONObject(data);
// Verarbeiten...
} catch (JSONException e) {
// Verwundbar: Zeigt Parsing-Details und Daten
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"JSON-Parsing fehlgeschlagen an Position " + e.getMessage() + " in: " + data);
}
}
}
Lösungscode
// Behoben: Sicheres Dateilesen mit bereinigten Fehlern
public class SichererFileReader {
private static final Logger logger = LoggerFactory.getLogger(SichererFileReader.class);
private static final String CONFIG_DIR = "/opt/app/config/";
public String readConfig(String filename) throws ConfigException {
// Behoben: Dateinamen validieren zur Pfad-Traversal-Verhinderung
if (filename == null || filename.contains("..") || filename.contains("/")) {
throw new ConfigException("Ungültiger Dateiname");
}
String fullPath = CONFIG_DIR + filename;
try {
FileReader reader = new FileReader(fullPath);
BufferedReader br = new BufferedReader(reader);
return br.readLine();
} catch (FileNotFoundException e) {
// Behoben: Details protokollieren, generische Meldung zurückgeben
logger.error("Konfigurationsdatei nicht gefunden: {}", filename, e);
throw new ConfigException("Konfigurationsdatei nicht verfügbar");
} catch (IOException e) {
logger.error("Fehler beim Lesen der Konfigurationsdatei: {}", filename, e);
throw new ConfigException("Konfiguration kann nicht gelesen werden");
}
}
}
// Behoben: Benutzerdefinierte Ausnahme mit separaten internen/externen Meldungen
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;
}
}
// Behoben: Sichere Authentifizierung ohne Anmeldedaten-Offenlegung
public class SichererAuthService {
private static final Logger logger = LoggerFactory.getLogger(SichererAuthService.class);
public AuthResult authenticate(String username, String password) {
// Behoben: Versuch ohne Anmeldedaten protokollieren
logger.info("Authentifizierungsversuch für Benutzer: {}", sanitizeForLog(username));
User user = userRepository.findByUsername(username);
if (user == null) {
// Behoben: Generische Meldung, nicht offenbaren ob Benutzer existiert
logger.warn("Fehlgeschlagene Authentifizierung: Benutzer nicht gefunden - {}", sanitizeForLog(username));
return AuthResult.failure("Ungültiger Benutzername oder Passwort");
}
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
// Behoben: Passwort niemals protokollieren oder offenlegen
logger.warn("Fehlgeschlagene Authentifizierung: ungültiges Passwort für Benutzer - {}",
sanitizeForLog(username));
return AuthResult.failure("Ungültiger Benutzername oder Passwort");
}
logger.info("Erfolgreiche Authentifizierung für Benutzer: {}", sanitizeForLog(username));
return AuthResult.success(user);
}
private String sanitizeForLog(String input) {
if (input == null) return "[null]";
// Potenziell gefährliche Zeichen für Log-Injection entfernen
return input.replaceAll("[\\r\\n\\t]", "_").substring(0, Math.min(input.length(), 50));
}
}
// Behoben: Ergebnisobjekt statt ausnahmebasiertem Ablauf
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);
}
// Getter...
}
// Behoben: Sicherer Datenbankservice mit Fehlerbehandlung
public class SichererDatabaseService {
private static final Logger logger = LoggerFactory.getLogger(SichererDatabaseService.class);
private final DataSource dataSource;
public SichererDatabaseService(DataSource dataSource) {
this.dataSource = dataSource;
}
public Connection getConnection() throws DatabaseException {
try {
return dataSource.getConnection();
} catch (SQLException e) {
// Behoben: Vollständige Details protokollieren, generische Meldung zurückgeben
logger.error("Datenbankverbindung fehlgeschlagen", e);
throw new DatabaseException("Verbindung zur Datenbank nicht möglich. Bitte versuchen Sie es später erneut.");
}
}
public List<User> findUsersByName(String name) throws DatabaseException {
// Behoben: Parametrisierte Abfragen verwenden
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) {
// Behoben: Details protokollieren ohne Abfragestruktur offenzulegen
String errorId = generateErrorId();
logger.error("Datenbankabfrage fehlgeschlagen [{}]", errorId, e);
throw new DatabaseException(
"Benutzerdaten können nicht abgerufen werden. Fehlerreferenz: " + errorId);
}
}
private String generateErrorId() {
return UUID.randomUUID().toString().substring(0, 8);
}
}
// Behoben: Sicherer REST-Controller mit ordnungsgemäßer Fehlerbehandlung
@RestController
@ControllerAdvice
public class SichererController {
private static final Logger logger = LoggerFactory.getLogger(SichererController.class);
@GetMapping("/user/{id}")
public ResponseEntity<User> getUser(@PathVariable String id) {
// Behoben: Eingabe validieren
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) {
// Behoben: Generischer Fehler, keine Datenoffenlegung
logger.warn("Ungültiges JSON empfangen", e);
return ResponseEntity.badRequest()
.body(new ProcessResult(false, "Ungültiges JSON-Format"));
}
}
// Behoben: Globaler Ausnahmehandler
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
String errorId = UUID.randomUUID().toString().substring(0, 8);
logger.error("Unbehandelte Ausnahme [{}]", errorId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(
"Ein Fehler ist aufgetreten. Referenz: " + errorId,
errorId
));
}
}
// Behoben: Fehlerantwort-Klasse
public class ErrorResponse {
private final String message;
private final String errorId;
public ErrorResponse(String message, String errorId) {
this.message = message;
this.errorId = errorId;
}
// Getter...
}
CVE-Beispiele
Keine spezifischen CVEs sind in der MITRE-Datenbank für dieses CWE gelistet. Java-Laufzeitfehler-Informationsoffenlegung wird jedoch häufig beobachtet bei:
- Spring Boot-Anwendungen mit aktivierter ausführlicher Fehlerbehandlung
- Falsch konfigurierten Java-Webanwendungen
- Debug-Modus-Konfigurationen in Produktion
Referenzen
- 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."