Verwendung innerer Klassen mit sensiblen Daten
Beschreibung
Verwendung innerer Klassen mit sensiblen Daten ist eine Schwachstelle, bei der innere Klassen verwendet werden, um sensible Informationen zu halten oder darauf zuzugreifen. Javas Compiler transformiert innere Klassen in separate Peer-Klassen mit Paketzugriffsebene, was potenziell Code und Daten offenlegt, die der Programmierer privat halten wollte. Wenn innere Klassen auf private Felder in ihrer umschließenden Klasse zugreifen, kann der Compiler diese privaten Felder in geschützte oder paket-private Felder umwandeln, was Sicherheitslücken erzeugt. Dies geschieht, weil Java-Bytecode kein Konzept von inneren Klassen hat — sie sind rein ein Quellcode-Konstrukt.
Risiko
Innere Klassen erzeugen versteckte Sicherheitsrisiken, weil ihre kompilierte Form sich erheblich von ihrer Quelldarstellung unterscheidet. Private Daten, auf die innere Klassen zugreifen, werden für jede Klasse im selben Paket zugänglich. Angreifer können Klassen im selben Paket erstellen, um auf vermeintlich private Felder und Methoden zuzugreifen, die durch die Kompilierung innerer Klassen offengelegt wurden. Sensible Daten wie Anmeldedaten, kryptographische Schlüssel oder persönliche Informationen, die in inneren Klassen gespeichert oder von ihnen aus umschließenden Klassen abgerufen werden, können von bösartigem Code gelesen oder modifiziert werden. Das Risiko wird verstärkt, weil Entwickler oft annehmen, dass Zugriffsmodifikatoren innerer Klassen Sicherheitsgarantien bieten, die sie tatsächlich nicht bieten.
Lösung
Vermeiden Sie die Verwendung innerer Klassen zum Speichern oder Zugreifen auf sensible Daten. Verwenden Sie versiegelte Klassen wo verfügbar, um die Kapselung zu schützen. Machen Sie innere Klassen statisch, wenn sie keinen Zugriff auf Instanzmitglieder der umschließenden Klasse benötigen, da statische innere Klassen vorhersehbarere Zugriffssemantik haben. Erwägen Sie die Verwendung lokaler innerer Klassen, die innerhalb von Methoden definiert sind, wenn die Klasse nur in einem bestimmten Bereich benötigt wird. Für wirklich sensible Operationen verwenden Sie separate Klassen mit ordnungsgemäßer Kapselung anstatt sich auf Zugriffsbeschränkungen innerer Klassen zu verlassen. Nehmen Sie niemals an, dass innere Klassen Sicherheitsisolation bieten.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Umfang: Vertraulichkeit Anwendungsdaten lesen - Die Vertraulichkeit von Daten innerer Klassen kann überwunden werden, was Angreifern ermöglicht, sensible Anwendungsdaten durch den offengelegten Paketzugriff zu lesen. |
| Integrität | Umfang: Integrität Anwendungsdaten modifizieren - Angreifer können Daten in inneren Klassen oder Daten, auf die innere Klassen aus äußeren Klassen zugreifen, durch die offengelegten Zugriffspfade modifizieren. |
Beispielcode
Anfälliger Code
// Anfällig: Innere Klasse greift auf private sensible Daten zu
public class VulnerableBankingSystem {
// Privates Feld - aber Zugriff durch innere Klasse macht es paket-zugänglich
private String masterEncryptionKey = "super-geheimer-schlüssel-12345";
private Map<String, Double> accountBalances = new HashMap<>();
// Anfällig: Nicht-statische innere Klasse hat Zugriff auf alle privaten Mitglieder
public class TransactionProcessor {
public void processTransaction(String accountId, double amount) {
// Innere Klasse kann auf private Felder der äußeren Klasse zugreifen
// Nach Kompilierung wird masterEncryptionKey paket-zugänglich
String encrypted = encrypt(amount, masterEncryptionKey);
// accountBalances ist ebenfalls offengelegt
double currentBalance = accountBalances.get(accountId);
accountBalances.put(accountId, currentBalance + amount);
}
private String encrypt(double amount, String key) {
// Verschlüsselungslogik
return "" + amount;
}
}
}
// Angreifer im selben Paket kann offengelegte Felder ausnutzen
package com.banking; // Gleiches Paket wie VulnerableBankingSystem
public class Attacker {
public void exploit() {
VulnerableBankingSystem system = new VulnerableBankingSystem();
// Aufgrund der Kompilierung innerer Klassen sind private Felder jetzt zugänglich
// durch Reflection oder synthetische Accessor-Methoden
try {
Field keyField = VulnerableBankingSystem.class
.getDeclaredField("masterEncryptionKey");
keyField.setAccessible(true); // Durch Zugriff innerer Klasse erleichtert
String key = (String) keyField.get(system);
System.out.println("Gestohlener Schlüssel: " + key);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Anfällig: Applet mit innerer Klasse, die sensible Daten offenlegt
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
public class VulnerableApplet extends Applet {
// Private sensible Daten
private String userPassword;
private String sessionToken;
// Anfällig: Anonyme innere Klasse für Ereignisbehandlung
// greift auf private Felder zu, legt sie auf Bytecode-Ebene offen
public void init() {
Button submitButton = new Button("Absenden");
// Anfällig: Anonyme innere Klasse
submitButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// Zugriff auf privates Feld legt es im Bytecode offen
authenticateUser(userPassword);
// sessionToken wird ebenfalls zugänglich
startSession(sessionToken);
}
});
add(submitButton);
}
private void authenticateUser(String password) {
// Authentifizierungslogik
}
private void startSession(String token) {
// Session-Logik
}
}
// Anfällig: Klasse mit sensibler innerer Klasse Daten
public class VulnerableUserManager {
// Anfällig: Innere Klasse hält sensible Benutzerdaten
private class UserCredentials {
String username;
String password;
String ssn;
String creditCardNumber;
UserCredentials(String user, String pass, String ssn, String cc) {
this.username = user;
this.password = pass;
this.ssn = ssn;
this.creditCardNumber = cc;
}
}
private UserCredentials currentUser;
public void login(String user, String pass, String ssn, String cc) {
// Anfällig: Sensible Daten in innerer Klasse
// ist zugänglicher als es im Quellcode erscheint
currentUser = new UserCredentials(user, pass, ssn, cc);
}
}
Korrigierter Code
// Korrigiert: Statische innere Klasse oder separate Klasse verwenden
public class SecureBankingSystem {
private final String masterEncryptionKey;
private final Map<String, Double> accountBalances;
public SecureBankingSystem() {
this.masterEncryptionKey = loadKeyFromSecureStorage();
this.accountBalances = new ConcurrentHashMap<>();
}
// Korrigiert: Statische innere Klasse hat keinen impliziten Zugriff auf äußere Klasse
public static class TransactionProcessor {
private final EncryptionService encryptionService;
private final AccountRepository accountRepository;
public TransactionProcessor(EncryptionService encryption,
AccountRepository accounts) {
this.encryptionService = encryption;
this.accountRepository = accounts;
}
public void processTransaction(String accountId, double amount) {
// Korrigiert: Verwendet injizierte Abhängigkeiten, nicht Felder der äußeren Klasse
String encrypted = encryptionService.encrypt(amount);
accountRepository.updateBalance(accountId, amount);
}
}
// Korrigiert: Factory-Methode bietet kontrollierten Zugriff
public TransactionProcessor createProcessor() {
return new TransactionProcessor(
new EncryptionService(masterEncryptionKey),
new AccountRepository(accountBalances)
);
}
private String loadKeyFromSecureStorage() {
// Aus HSM oder sicherem Tresor laden
return SecureKeyVault.getKey("master-key");
}
}
// Korrigiert: Lokale innere Klasse für begrenzten Bereich verwenden
public class SecureApplet extends Applet {
private char[] userPassword; // char[] für Passwörter, nicht String
public void init() {
Button submitButton = new Button("Absenden");
// Korrigiert: Methode behandelt Authentifizierung direkt
submitButton.addActionListener(this::handleSubmit);
add(submitButton);
}
// Korrigiert: ActionListener direkt auf der Klasse implementieren
private void handleSubmit(ActionEvent e) {
try {
// Passwort lokal verarbeiten
authenticateUser(userPassword);
} finally {
// Sensible Daten nach Verwendung löschen
Arrays.fill(userPassword, '\0');
}
}
private void authenticateUser(char[] password) {
// Authentifizierungslogik
}
}
// Alternative: Schnittstelle direkt implementieren
public class SecureApplet2 extends Applet implements ActionListener {
private char[] userPassword;
public void init() {
Button submitButton = new Button("Absenden");
submitButton.addActionListener(this);
add(submitButton);
}
@Override
public void actionPerformed(ActionEvent e) {
// Korrigiert: Keine innere Klasse, direkte Implementierung
authenticateUser(userPassword);
}
private void authenticateUser(char[] password) {
// Authentifizierungslogik
}
}
// Korrigiert: Separate Klasse für sensible Daten mit ordnungsgemäßer Kapselung
public final class SecureUserManager {
// Korrigiert: Anmeldedaten in separater, versiegelter Klasse gespeichert
private SecureCredentials currentUser;
public void login(String user, char[] pass) {
// Korrigiert: Ordnungsgemäß gekapseltes Anmeldedatenobjekt verwenden
currentUser = SecureCredentials.create(user, pass);
// Passwort-Array nach Verwendung löschen
Arrays.fill(pass, '\0');
}
public boolean isAuthenticated() {
return currentUser != null && currentUser.isValid();
}
}
// Korrigiert: Separate finale Klasse für Anmeldedaten (keine innere Klasse)
public final class SecureCredentials {
private final String username;
private final byte[] hashedPassword;
private final Instant createdAt;
private SecureCredentials(String username, byte[] hashedPassword) {
this.username = username;
this.hashedPassword = hashedPassword;
this.createdAt = Instant.now();
}
public static SecureCredentials create(String username, char[] password) {
byte[] hashed = hashPassword(password);
// Ursprüngliches Passwort löschen
Arrays.fill(password, '\0');
return new SecureCredentials(username, hashed);
}
public boolean isValid() {
// Prüfen ob Anmeldedaten nicht abgelaufen sind
return createdAt.plusSeconds(3600).isAfter(Instant.now());
}
public boolean verifyPassword(char[] password) {
byte[] hashed = hashPassword(password);
boolean matches = MessageDigest.isEqual(hashedPassword, hashed);
Arrays.fill(hashed, (byte) 0);
return matches;
}
private static byte[] hashPassword(char[] password) {
// Ordnungsgemäßes Passwort-Hashing verwenden (bcrypt, Argon2, etc.)
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] bytes = new String(password).getBytes(StandardCharsets.UTF_8);
byte[] hash = md.digest(bytes);
Arrays.fill(bytes, (byte) 0);
return hash;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
CVE-Beispiele
Keine spezifischen CVEs sind in der MITRE-Datenbank für dieses CWE aufgelistet. Das Schwachstellenmuster ist jedoch dokumentiert in:
- Seven Pernicious Kingdoms Taxonomie
- Java-Bytecode-Sicherheitsforschung
Referenzen
- MITRE Corporation. "CWE-492: Use of Inner Class Containing Sensitive Data." https://cwe.mitre.org/data/definitions/492.html
- Oracle. "Secure Coding Guidelines for Java SE."
- McGraw, Gary und Felten, Edward. "Securing Java."