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

AuswirkungDetails
IntegritätUmfang: 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.
ZugriffskontrolleUmfang: 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

  1. MITRE Corporation. "CWE-486: Comparison of Classes by Name." https://cwe.mitre.org/data/definitions/486.html
  2. Oracle. "Secure Coding Guidelines for Java SE."
  3. CERT Oracle Secure Coding Standard for Java. "OBJ09-J. Compare classes and not class names."