Kritische öffentliche Variable ohne Final-Modifikator

Beschreibung

Kritische öffentliche Variable ohne Final-Modifikator ist eine Schwachstelle, bei der ein Produkt eine kritische öffentliche Variable enthält, der der final-Modifikator fehlt, was unbefugte Modifikation auf unerwartete Werte ermöglicht. Nicht-finale öffentliche Felder können von jedem Code geändert werden, der Zugriff auf die enthaltende Klasse hat. Dies birgt Risiken, wenn andere Programmkomponenten sich auf bestimmte Feldwerte für Sicherheitsentscheidungen, Konfiguration oder die Aufrechterhaltung eines konsistenten Zustands verlassen. Angreifer können diese Felder manipulieren, um das Programmverhalten zu ändern, Sicherheitsprüfungen zu umgehen oder den Anwendungszustand zu korrumpieren.

Risiko

Veränderliche öffentliche Felder in sicherheitskritischen Klassen erzeugen direkte Angriffsvektoren. Angreifer können Preisinformationen in E-Commerce-Anwendungen modifizieren, Berechtigungsstufen in Zugriffskontrollsystemen ändern, Konfigurationspfade auf bösartige Dateien umleiten oder Zustandsvariablen manipulieren, um Sicherheitsprüfungen zu umgehen. In Applet- oder verteilten Umgebungen kann jeder Code, der in derselben JVM läuft, diese Felder modifizieren. Das Risiko ist besonders hoch für Felder, die Sicherheitsrichtlinien, Finanzberechnungen, Dateipfade oder Authentifizierungszustände kontrollieren. Selbst scheinbar harmlose Felder können zu Angriffsvektoren werden, wenn ihre Modifikation die Programmlogik beeinflusst.

Lösung

Deklarieren Sie alle öffentlichen Felder als final wenn möglich, besonders in sicherheitssensitiven Klassen und Applets. Für Felder, die veränderlich sein müssen, machen Sie sie privat und bieten Sie kontrollierten Zugriff durch Getter- und Setter-Methoden mit Validierung. Führen Sie Plausibilitätsprüfungen durch, bevor Sie Werte aus öffentlichen Feldern verwenden. Erwägen Sie die Verwendung unveränderlicher Objekte für komplexe Datentypen. Verwenden Sie Kapselungsmuster zum Schutz kritischer Zustände. Erwägen Sie in Java defensives Kopieren, wenn Sie veränderliche Objekte durch Getter offenlegen.

Häufige Auswirkungen

AuswirkungDetails
IntegritätUmfang: Integrität

Anwendungsdaten modifizieren - Das Objekt könnte potenziell manipuliert werden, was Angreifern ermöglicht, kritische Werte zu modifizieren, die das Programmverhalten beeinflussen.
VertraulichkeitUmfang: Vertraulichkeit

Anwendungsdaten lesen - Das Objekt könnte potenziell gelesen werden, was Angreifern ermöglicht, sensible Konfiguration oder Zustandsinformationen zu entdecken.
ZugriffskontrolleUmfang: Zugriffskontrolle

Schutzmechanismus umgehen - Modifizierung sicherheitsrelevanter Felder kann Authentifizierung, Autorisierung oder andere Sicherheitsprüfungen umgehen.

Beispielcode

Anfälliger Code

// Anfällig: E-Commerce-Preisfeld nicht final
public class VulnerableProduct {
    // Anfällig: Öffentlicher, nicht-finaler Preis kann modifiziert werden
    public double price = 99.99;
    public String name = "Premium Widget";
    public int discountPercent = 0;

    public double calculateTotal(int quantity) {
        // Anfällig: Preis könnte modifiziert worden sein
        double discount = price * discountPercent / 100.0;
        return (price - discount) * quantity;
    }
}

// Angreifer kann ausnutzen
public class PriceExploit {
    public void exploit() {
        VulnerableProduct product = new VulnerableProduct();

        // Preis vor Kauf modifizieren
        product.price = 0.01;  // Fast kostenlos!
        product.discountPercent = 99;  // Maximaler Rabatt

        double total = product.calculateTotal(1000);
        System.out.println("Gesamt: " + total);  // Zahlt fast nichts
    }
}

// Anfällig: Konfiguration mit öffentlichen nicht-finalen Feldern
public class VulnerableConfig {
    // Anfällig: Kritische Pfade können modifiziert werden
    public String configPath = "/etc/application/config.dat";
    public String logPath = "/var/log/application.log";
    public String uploadDirectory = "/var/uploads/";

    // Anfällig: Sicherheitseinstellungen können modifiziert werden
    public boolean requireAuthentication = true;
    public boolean enableEncryption = true;
    public int maxLoginAttempts = 3;
}

// Angreifer leitet Konfiguration um
public class ConfigExploit {
    public void exploit(VulnerableConfig config) {
        // Config zu angreiferkontrollierter Datei umleiten
        config.configPath = "/tmp/malicious-config.dat";

        // Sicherheit deaktivieren
        config.requireAuthentication = false;
        config.enableEncryption = false;
        config.maxLoginAttempts = Integer.MAX_VALUE;

        // Uploads zu webzugänglichem Verzeichnis umleiten
        config.uploadDirectory = "/var/www/html/uploads/";
    }
}
// Anfällig: Applet mit veränderlichen öffentlichen Daten
import java.applet.Applet;

public class VulnerableApplet extends Applet {
    // Anfällig: Öffentliche Felder im Applet
    public String serverUrl = "https://secure-server.com/api";
    public String apiKey = "geheimer-api-schlüssel-12345";
    public boolean isAdmin = false;

    public void makeRequest() {
        // Anfällig: Verwendet potenziell modifizierte Werte
        HttpClient.post(serverUrl, apiKey, getData());

        if (isAdmin) {
            // Angreifer kann Admin-Funktionen aktivieren
            enableAdminFeatures();
        }
    }
}

// Anfällig: Sicherheitskontext mit veränderlichen Feldern
public class VulnerableSecurityContext {
    // Anfällig: Authentifizierungszustand kann modifiziert werden
    public boolean isAuthenticated = false;
    public String userId = null;
    public String[] roles = new String[0];

    // Anfällig: Berechtigungsstufe kann modifiziert werden
    public int permissionLevel = 0;  // 0=keine, 1=lesen, 2=schreiben, 3=admin

    public boolean canPerformAction(String action) {
        // Entscheidung basiert auf potenziell modifizierten Feldern
        if (!isAuthenticated) {
            return false;
        }

        switch (action) {
            case "read":
                return permissionLevel >= 1;
            case "write":
                return permissionLevel >= 2;
            case "admin":
                return permissionLevel >= 3;
            default:
                return false;
        }
    }
}

// Angreifer eskaliert Privilegien
public class PrivilegeExploit {
    public void exploit(VulnerableSecurityContext ctx) {
        // Authentifiziert setzen ohne Anmeldung
        ctx.isAuthenticated = true;

        // Zu Admin eskalieren
        ctx.permissionLevel = 3;
        ctx.roles = new String[]{"ADMIN", "SUPER_USER"};
        ctx.userId = "admin";

        // Hat jetzt vollen Zugriff
        boolean canAdmin = ctx.canPerformAction("admin");  // true!
    }
}
// Anfällig: C++-Klasse mit öffentlichen nicht-const-Mitgliedern
class VulnerableAccount {
public:
    // Anfällig: Öffentliche, nicht-const kritische Daten
    double balance;
    std::string accountNumber;
    int accessLevel;
    bool isLocked;

    VulnerableAccount(std::string num) : accountNumber(num) {
        balance = 0.0;
        accessLevel = 1;
        isLocked = false;
    }

    void withdraw(double amount) {
        // Anfällig: balance könnte modifiziert worden sein
        if (!isLocked && amount <= balance) {
            balance -= amount;
        }
    }
};

// Angreifer modifiziert Konto
void exploit() {
    VulnerableAccount account("12345");

    // Angreifer modifiziert Kontostand direkt
    account.balance = 1000000.0;  // Kostenloses Geld!
    account.accessLevel = 999;     // Maximaler Zugriff
    account.isLocked = false;      // Entsperren falls gesperrt
}

Korrigierter Code

// Korrigiert: Unveränderliches Produkt mit finalen Feldern
public final class SecureProduct {
    // Korrigiert: Finale Felder können nach Konstruktion nicht modifiziert werden
    public final double price;
    public final String name;
    private final int discountPercent;

    public SecureProduct(String name, double price, int discountPercent) {
        if (price < 0) {
            throw new IllegalArgumentException("Preis kann nicht negativ sein");
        }
        if (discountPercent < 0 || discountPercent > 100) {
            throw new IllegalArgumentException("Ungültiger Rabatt");
        }
        this.name = name;
        this.price = price;
        this.discountPercent = discountPercent;
    }

    public double calculateTotal(int quantity) {
        // Korrigiert: Preis ist unveränderlich
        double discount = price * discountPercent / 100.0;
        return (price - discount) * quantity;
    }

    public int getDiscountPercent() {
        return discountPercent;
    }
}

// Korrigiert: Konfiguration mit privaten Feldern und Validierung
public final class SecureConfig {
    // Korrigiert: Private finale Felder
    private final String configPath;
    private final String logPath;
    private final String uploadDirectory;
    private final boolean requireAuthentication;
    private final boolean enableEncryption;
    private final int maxLoginAttempts;

    private SecureConfig(Builder builder) {
        this.configPath = builder.configPath;
        this.logPath = builder.logPath;
        this.uploadDirectory = builder.uploadDirectory;
        this.requireAuthentication = builder.requireAuthentication;
        this.enableEncryption = builder.enableEncryption;
        this.maxLoginAttempts = builder.maxLoginAttempts;
    }

    // Korrigiert: Getter bieten nur Lesezugriff
    public String getConfigPath() { return configPath; }
    public String getLogPath() { return logPath; }
    public String getUploadDirectory() { return uploadDirectory; }
    public boolean isRequireAuthentication() { return requireAuthentication; }
    public boolean isEnableEncryption() { return enableEncryption; }
    public int getMaxLoginAttempts() { return maxLoginAttempts; }

    // Korrigiert: Builder-Muster für kontrollierte Konstruktion
    public static class Builder {
        private String configPath = "/etc/application/config.dat";
        private String logPath = "/var/log/application.log";
        private String uploadDirectory = "/var/uploads/";
        private boolean requireAuthentication = true;
        private boolean enableEncryption = true;
        private int maxLoginAttempts = 3;

        public Builder configPath(String path) {
            // Korrigiert: Pfad validieren
            if (!isValidPath(path)) {
                throw new IllegalArgumentException("Ungültiger Konfigurationspfad");
            }
            this.configPath = path;
            return this;
        }

        public SecureConfig build() {
            return new SecureConfig(this);
        }

        private boolean isValidPath(String path) {
            // Validieren dass Pfad keine Traversierung enthält
            return !path.contains("..") && path.startsWith("/etc/");
        }
    }
}
// Korrigiert: Sicheres Applet mit Kapselung
import java.applet.Applet;

public final class SecureApplet extends Applet {
    // Korrigiert: Private finale Felder
    private final String serverUrl;
    private final String apiKey;
    private boolean isAdmin;  // Intern kontrolliert

    public SecureApplet() {
        // Korrigiert: Aus sicherer Quelle laden
        this.serverUrl = loadServerUrl();
        this.apiKey = loadApiKey();
        this.isAdmin = false;  // Standard auf Nicht-Admin
    }

    // Korrigiert: Keine Setter für kritische Felder
    // Admin-Status durch Authentifizierung bestimmt, nicht öffentliches Feld

    public void authenticate(String username, String password) {
        // Korrigiert: isAdmin nur durch ordnungsgemäße Authentifizierung gesetzt
        AuthResult result = AuthService.authenticate(username, password);
        if (result.isSuccess()) {
            this.isAdmin = result.hasAdminRole();
        }
    }

    public boolean isAdmin() {
        return isAdmin;
    }

    private String loadServerUrl() {
        // Aus signierter Konfiguration laden
        return SignedConfig.get("server.url");
    }

    private String loadApiKey() {
        // Aus sicherem Speicher laden
        return SecureStorage.getApiKey();
    }
}

// Korrigiert: Unveränderlicher Sicherheitskontext
public final class SecureSecurityContext {
    // Korrigiert: Alle Felder privat und final
    private final boolean isAuthenticated;
    private final String userId;
    private final Set<String> roles;
    private final int permissionLevel;

    private SecureSecurityContext(String userId, Set<String> roles,
                                  int permissionLevel) {
        this.isAuthenticated = (userId != null);
        this.userId = userId;
        this.roles = Collections.unmodifiableSet(new HashSet<>(roles));
        this.permissionLevel = permissionLevel;
    }

    // Korrigiert: Factory-Methode für nicht-authentifizierten Kontext
    public static SecureSecurityContext anonymous() {
        return new SecureSecurityContext(null, Collections.emptySet(), 0);
    }

    // Korrigiert: Factory-Methode erstellt Kontext aus Authentifizierungsergebnis
    public static SecureSecurityContext fromAuthentication(AuthResult result) {
        if (!result.isSuccess()) {
            return anonymous();
        }
        return new SecureSecurityContext(
            result.getUserId(),
            result.getRoles(),
            calculatePermissionLevel(result.getRoles())
        );
    }

    public boolean canPerformAction(String action) {
        if (!isAuthenticated) {
            return false;
        }

        switch (action) {
            case "read":
                return permissionLevel >= 1;
            case "write":
                return permissionLevel >= 2;
            case "admin":
                return permissionLevel >= 3;
            default:
                return false;
        }
    }

    // Korrigiert: Nur Getter, keine Setter
    public boolean isAuthenticated() { return isAuthenticated; }
    public String getUserId() { return userId; }
    public Set<String> getRoles() { return roles; }

    private static int calculatePermissionLevel(Set<String> roles) {
        if (roles.contains("ADMIN")) return 3;
        if (roles.contains("EDITOR")) return 2;
        if (roles.contains("VIEWER")) return 1;
        return 0;
    }
}
// Korrigiert: C++-Klasse mit ordnungsgemäßer Kapselung
class SecureAccount {
private:
    // Korrigiert: Private Mitglieder mit kontrolliertem Zugriff
    double balance_;
    const std::string accountNumber_;
    int accessLevel_;
    bool isLocked_;

public:
    SecureAccount(const std::string& num)
        : accountNumber_(num), balance_(0.0),
          accessLevel_(1), isLocked_(false) {}

    // Korrigiert: Kontrollierter Zugriff durch Methoden
    double getBalance() const { return balance_; }

    bool withdraw(double amount) {
        if (isLocked_ || amount <= 0 || amount > balance_) {
            return false;
        }
        balance_ -= amount;
        auditLog("Abhebung", amount);
        return true;
    }

    bool deposit(double amount) {
        if (isLocked_ || amount <= 0) {
            return false;
        }
        balance_ += amount;
        auditLog("Einzahlung", amount);
        return true;
    }

    // Korrigiert: Sperren/Entsperren erfordert Authentifizierung
    bool lock(const AuthToken& token) {
        if (!token.hasPermission("LOCK_ACCOUNTS")) {
            return false;
        }
        isLocked_ = true;
        return true;
    }

private:
    void auditLog(const std::string& action, double amount) {
        // Transaktion für Audit protokollieren
    }
};

CVE-Beispiele

Keine spezifischen CVEs sind in der MITRE-Datenbank für dieses CWE aufgelistet. Das Schwachstellenmuster ist jedoch gut dokumentiert in:

  • Java-Sicherheitsrichtlinien
  • CERT Secure Coding Standards

Referenzen

  1. MITRE Corporation. "CWE-493: Critical Public Variable Without Final Modifier." https://cwe.mitre.org/data/definitions/493.html
  2. CERT Oracle Secure Coding Standard for Java. "OBJ10-J. Do not use public static nonfinal fields."
  3. Oracle. "Secure Coding Guidelines for Java SE."