Vergleich von Klassen nach Namen
Beschreibung
Vergleich von Klassen nach Namen ist eine Schwachstelle, bei der Code Klassen anhand ihrer String-Namen vergleicht anstatt der tatsächlichen Klassenidentität. Wenn Vertrauensentscheidungen auf Klassennamen basieren anstatt die tatsächliche Klassenidentität zu verifizieren, können Angreifer bösartige Klassen mit Namen erstellen, die vertrauenswürdigen Klassen entsprechen, um diese Vertrauensbeziehung auszunutzen. Mehrere Klassen können identische Namen haben, wenn sie von verschiedenen Classloadern oder aus verschiedenen Paketen geladen werden, was namensbasierten Vergleich für Sicherheitsentscheidungen inhärent unzuverlässig macht.
Risiko
Namensbasierter Klassenvergleich erzeugt schwerwiegende Sicherheitslücken in Java-Anwendungen. Angreifer können bösartige Klassen mit Namen erstellen, die identisch mit vertrauenswürdigen Klassen sind, und dann Objekte dieser bösartigen Klassen einreichen, um Sicherheitsprüfungen zu umgehen. In Java können verschiedene Classloader unterschiedliche Klassen mit demselben vollqualifizierten Namen laden, und diese Klassen sind nicht gleich, obwohl ihre Namen übereinstimmen. Dies ermöglicht Angreifern, bösartigen Code einzuschleusen, der namensbasierte Vertrauensprüfungen besteht. Die Schwachstelle ist besonders gefährlich in Plugin-Systemen, Deserialisierungskontexten und jedem Code, der Klassen dynamisch lädt und validiert.
Lösung
Verwenden Sie Klassenäquivalenzprüfung mit der getClass()-Methode und dem ==-Operator, um die tatsächliche Typidentität zu verifizieren. Vergleichen Sie gegen das tatsächliche Klassenobjekt anstatt seinen Namen. Verwenden Sie zum Beispiel obj.getClass() == TrustedClass.class anstatt Namens-Strings zu vergleichen. Bei der Validierung von Klassenhierarchien verwenden Sie instanceof oder Class.isAssignableFrom(). Verwenden Sie niemals Klassennamen für sicherheitskritische Entscheidungen. Wenn Klassennamensvergleich unvermeidbar ist, verifizieren Sie auch den Classloader, um sicherzustellen, dass die Klasse aus einer vertrauenswürdigen Quelle stammt.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Umfang: Integrität, Vertraulichkeit, Verfügbarkeit Unbefugten Code ausführen - Das System kann falschen oder unbeabsichtigten Code ausführen, wenn es sich allein auf Klassennamenidentität verlässt, was Angreifern ermöglicht, bösartige Klassen einzusetzen. |
| Zugriffskontrolle | Umfang: Zugriffskontrolle Schutzmechanismus umgehen - Angreifer können typbasierte Sicherheitsprüfungen umgehen, indem sie bösartige Objekte mit übereinstimmenden Klassennamen bereitstellen. |
Beispielcode
Anfälliger Code
// Anfällig: Vergleich von Klassen nach Namen
public class VulnerableClassComparison {
// Anfällig: Namensbasierter Klassenvergleich für Vertrauensentscheidung
public void processObject(Object inputObject) {
// Anfällig: Eine Angreiferklasse mit demselben Namen besteht diese Prüfung
if (inputObject.getClass().getName().equals("com.trusted.SecurityToken")) {
// Angreifer kann eigene SecurityToken-Klasse erstellen
// und sie wird diese Prüfung bestehen
SecurityToken token = (SecurityToken) inputObject;
grantAccess(token);
}
}
// Anfällig: String-Vergleich von Klassennamen
public boolean isTrustedClass(Class<?> clazz) {
// Anfällig: Mehrere Klassen können denselben Namen haben
String className = clazz.getName();
return className.equals("TrustedProcessor") ||
className.equals("SecureHandler") ||
className.equals("AuthenticatedUser");
}
// Anfällig: Einfacher Namensvergleich (noch schlimmer)
public void validatePlugin(Object plugin) {
// Anfällig: Prüft nur einfachen Namen, ignoriert Paket
if (plugin.getClass().getSimpleName().equals("SafePlugin")) {
// Jede Klasse namens "SafePlugin" aus jedem Paket besteht
executePlugin(plugin);
}
}
// Anfällig: Deserialisierung mit namensbasierter Validierung
public Object deserializeObject(byte[] data, String expectedClassName)
throws Exception {
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data)
);
Object obj = ois.readObject();
// Anfällig: Angreifer kontrolliert sowohl serialisierte Daten
// als auch kann bösartige Klasse mit erwartetem Namen erstellen
if (obj.getClass().getName().equals(expectedClassName)) {
return obj;
}
throw new SecurityException("Unerwartete Klasse");
}
// Anfällig: Klassen-Whitelist mit Namen
private static final Set<String> ALLOWED_CLASSES = Set.of(
"java.lang.String",
"java.lang.Integer",
"com.app.SafeData"
);
public void processWithWhitelist(Object obj) {
// Anfällig: Eigene Klasse mit demselben Namen wie Whitelist-Klasse
if (ALLOWED_CLASSES.contains(obj.getClass().getName())) {
// Vertraut jeder Klasse mit übereinstimmendem Namen
process(obj);
}
}
}
// Anfällig: Plugin-System mit namensbasiertem Vertrauen
public class VulnerablePluginLoader {
public void loadPlugin(String pluginPath) throws Exception {
URLClassLoader loader = new URLClassLoader(
new URL[]{new File(pluginPath).toURI().toURL()}
);
Class<?> pluginClass = loader.loadClass("com.plugins.Plugin");
// Anfällig: Prüft nur ob Klassenname erwarteter Schnittstelle entspricht
if (pluginClass.getName().endsWith("Plugin")) {
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
// Bösartiges Plugin des Angreifers hat erwarteten Namen
runPlugin(plugin);
}
}
// Anfällig: Schnittstellenprüfung nach Namen
public boolean implementsInterface(Object obj, String interfaceName) {
for (Class<?> iface : obj.getClass().getInterfaces()) {
// Anfällig: Vergleich von Schnittstellennamen
if (iface.getName().equals(interfaceName)) {
return true;
}
}
return false;
}
}
Korrigierter Code
// Korrigiert: Vergleich von Klassen nach Identität
public class SecureClassComparison {
// Korrigiert: Klassenidentitätsvergleich verwenden
public void processObject(Object inputObject) {
// Korrigiert: Tatsächliche Klassenobjekte vergleichen, nicht Namen
if (inputObject.getClass() == SecurityToken.class) {
// Nur die tatsächliche SecurityToken-Klasse aus unserem Classloader besteht
SecurityToken token = (SecurityToken) inputObject;
grantAccess(token);
}
}
// Korrigiert: Class-Objekt-Vergleich verwenden
public boolean isTrustedClass(Class<?> clazz) {
// Korrigiert: Gegen tatsächliche Klassenobjekte vergleichen
return clazz == TrustedProcessor.class ||
clazz == SecureHandler.class ||
clazz == AuthenticatedUser.class;
}
// Korrigiert: instanceof für Typprüfung verwenden
public void validatePlugin(Object plugin) {
// Korrigiert: instanceof prüft tatsächliche Typhierarchie
if (plugin instanceof SafePlugin) {
// Nur tatsächliche SafePlugin-Instanzen bestehen
executePlugin((SafePlugin) plugin);
}
}
// Korrigiert: Klassenbasierte Validierung mit Classloader-Verifizierung
public Object deserializeObject(byte[] data, Class<?> expectedClass)
throws Exception {
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data)
);
Object obj = ois.readObject();
// Korrigiert: Klassenobjekte vergleichen und Classloader verifizieren
if (obj.getClass() == expectedClass &&
obj.getClass().getClassLoader() == expectedClass.getClassLoader()) {
return obj;
}
throw new SecurityException("Unerwartete Klasse oder Classloader");
}
// Korrigiert: Klassen-Whitelist mit tatsächlichen Class-Objekten
private static final Set<Class<?>> ALLOWED_CLASSES = Set.of(
String.class,
Integer.class,
SafeData.class
);
public void processWithWhitelist(Object obj) {
// Korrigiert: Gegen tatsächliche Klassenobjekte prüfen
if (ALLOWED_CLASSES.contains(obj.getClass())) {
process(obj);
}
}
// Korrigiert: Typsichere Verarbeitung mit Generics
public <T> void processTypeSafe(T obj, Class<T> expectedType) {
// Korrigiert: Tatsächliche Klasse verifizieren
if (expectedType.isInstance(obj)) {
T typedObj = expectedType.cast(obj);
processInternal(typedObj);
}
}
}
// Korrigiert: Sicheres Plugin-System
public class SecurePluginLoader {
// Korrigiert: Erwartete Schnittstelle definieren
private static final Class<Plugin> PLUGIN_INTERFACE = Plugin.class;
public void loadPlugin(String pluginPath) throws Exception {
URLClassLoader loader = new URLClassLoader(
new URL[]{new File(pluginPath).toURI().toURL()},
getClass().getClassLoader() // Eltern-Classloader setzen
);
Class<?> pluginClass = loader.loadClass("com.plugins.ConcretePlugin");
// Korrigiert: Prüfen ob Klasse unsere Plugin-Schnittstelle implementiert
if (PLUGIN_INTERFACE.isAssignableFrom(pluginClass)) {
Plugin plugin = (Plugin) pluginClass
.getDeclaredConstructor()
.newInstance();
// Korrigiert: Zusätzliche Classloader-Verifizierung
if (verifyClassLoader(plugin.getClass())) {
runPlugin(plugin);
}
}
}
// Korrigiert: Schnittstellenprüfung nach Klassenidentität
public boolean implementsInterface(Object obj, Class<?> interfaceClass) {
// Korrigiert: isAssignableFrom für ordnungsgemäße Typprüfung verwenden
return interfaceClass.isAssignableFrom(obj.getClass());
}
// Korrigiert: Verifizieren dass Klasse aus vertrauenswürdigem Classloader stammt
private boolean verifyClassLoader(Class<?> clazz) {
ClassLoader loader = clazz.getClassLoader();
while (loader != null) {
if (loader == getClass().getClassLoader() ||
loader == ClassLoader.getSystemClassLoader()) {
return true;
}
loader = loader.getParent();
}
return loader == null; // Bootstrap-Classloader ist vertrauenswürdig
}
}
CVE-Beispiele
Keine spezifischen CVEs sind in der MITRE-Datenbank für dieses CWE aufgelistet. Das Schwachstellenmuster ist jedoch dokumentiert in:
- Java-Sicherheitsrichtlinien und Best Practices
- Deserialisierungs-Schwachstellenforschung (bezogen auf CWE-502)
Referenzen
- MITRE Corporation. "CWE-486: Comparison of Classes by Name." https://cwe.mitre.org/data/definitions/486.html
- Oracle. "Secure Coding Guidelines for Java SE."
- CERT Oracle Secure Coding Standard for Java. "OBJ09-J. Compare classes and not class names."