Critical Public Variable Without Final Modifier

Description

Critical Public Variable Without Final Modifier is a vulnerability where a product contains a critical public variable that lacks the final modifier, allowing unauthorized modification to unexpected values. Non-final public fields can be altered by any code that has access to the containing class. This poses risks when other program components rely on specific field values for security decisions, configuration, or maintaining consistent state. Attackers can manipulate these fields to alter program behavior, bypass security checks, or corrupt application state.

Risk

Mutable public fields in security-critical classes create direct attack vectors. Attackers can modify pricing information in e-commerce applications, alter permission levels in access control systems, change configuration paths to point to malicious files, or manipulate state variables to bypass security checks. In applet or distributed environments, any code running in the same JVM can modify these fields. The risk is particularly high for fields that control security policies, financial calculations, file paths, or authentication states. Even seemingly innocuous fields can become attack vectors when their modification affects program logic.

Solution

Declare all public fields as final when possible, especially in security-sensitive classes and applets. For fields that must be mutable, make them private and provide controlled access through getter and setter methods with validation. Perform sanity checks before using values from public fields. Consider using immutable objects for complex data types. Use encapsulation patterns to protect critical state. In Java, consider using defensive copying when exposing mutable objects through getters.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Modify Application Data - The object could potentially be tampered with, allowing attackers to modify critical values that affect program behavior.
ConfidentialityScope: Confidentiality

Read Application Data - The object could potentially be read, allowing attackers to discover sensitive configuration or state information.
Access ControlScope: Access Control

Bypass Protection Mechanism - Modifying security-related fields can bypass authentication, authorization, or other security checks.

Example Code

Vulnerable Code

// Vulnerable: E-commerce price field not final
public class VulnerableProduct {
    // Vulnerable: Public, non-final price can be modified
    public double price = 99.99;
    public String name = "Premium Widget";
    public int discountPercent = 0;

    public double calculateTotal(int quantity) {
        // Vulnerable: price could have been modified
        double discount = price * discountPercent / 100.0;
        return (price - discount) * quantity;
    }
}

// Attacker can exploit
public class PriceExploit {
    public void exploit() {
        VulnerableProduct product = new VulnerableProduct();

        // Modify price before purchase
        product.price = 0.01;  // Almost free!
        product.discountPercent = 99;  // Maximum discount

        double total = product.calculateTotal(1000);
        System.out.println("Total: " + total);  // Pays almost nothing
    }
}

// Vulnerable: Configuration with public non-final fields
public class VulnerableConfig {
    // Vulnerable: Critical paths can be modified
    public String configPath = "/etc/application/config.dat";
    public String logPath = "/var/log/application.log";
    public String uploadDirectory = "/var/uploads/";

    // Vulnerable: Security settings can be modified
    public boolean requireAuthentication = true;
    public boolean enableEncryption = true;
    public int maxLoginAttempts = 3;
}

// Attacker redirects configuration
public class ConfigExploit {
    public void exploit(VulnerableConfig config) {
        // Redirect config to attacker-controlled file
        config.configPath = "/tmp/malicious-config.dat";

        // Disable security
        config.requireAuthentication = false;
        config.enableEncryption = false;
        config.maxLoginAttempts = Integer.MAX_VALUE;

        // Redirect uploads to web-accessible directory
        config.uploadDirectory = "/var/www/html/uploads/";
    }
}
// Vulnerable: Applet with mutable public data
import java.applet.Applet;

public class VulnerableApplet extends Applet {
    // Vulnerable: Public fields in applet
    public String serverUrl = "https://secure-server.com/api";
    public String apiKey = "secret-api-key-12345";
    public boolean isAdmin = false;

    public void makeRequest() {
        // Vulnerable: Uses potentially modified values
        HttpClient.post(serverUrl, apiKey, getData());

        if (isAdmin) {
            // Attacker can enable admin features
            enableAdminFeatures();
        }
    }
}

// Vulnerable: Security context with mutable fields
public class VulnerableSecurityContext {
    // Vulnerable: Authentication state can be modified
    public boolean isAuthenticated = false;
    public String userId = null;
    public String[] roles = new String[0];

    // Vulnerable: Permission level can be modified
    public int permissionLevel = 0;  // 0=none, 1=read, 2=write, 3=admin

    public boolean canPerformAction(String action) {
        // Decision based on potentially modified fields
        if (!isAuthenticated) {
            return false;
        }

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

// Attacker escalates privileges
public class PrivilegeExploit {
    public void exploit(VulnerableSecurityContext ctx) {
        // Set authenticated without logging in
        ctx.isAuthenticated = true;

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

        // Now has full access
        boolean canAdmin = ctx.canPerformAction("admin");  // true!
    }
}
// Vulnerable: C++ class with public non-const members
class VulnerableAccount {
public:
    // Vulnerable: Public, non-const critical data
    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) {
        // Vulnerable: balance could have been modified
        if (!isLocked && amount <= balance) {
            balance -= amount;
        }
    }
};

// Attacker modifies account
void exploit() {
    VulnerableAccount account("12345");

    // Attacker modifies balance directly
    account.balance = 1000000.0;  // Free money!
    account.accessLevel = 999;     // Maximum access
    account.isLocked = false;      // Unlock if locked
}

Fixed Code

// Fixed: Immutable product with final fields
public final class SecureProduct {
    // Fixed: Final fields cannot be modified after construction
    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("Price cannot be negative");
        }
        if (discountPercent < 0 || discountPercent > 100) {
            throw new IllegalArgumentException("Invalid discount");
        }
        this.name = name;
        this.price = price;
        this.discountPercent = discountPercent;
    }

    public double calculateTotal(int quantity) {
        // Fixed: price is immutable
        double discount = price * discountPercent / 100.0;
        return (price - discount) * quantity;
    }

    public int getDiscountPercent() {
        return discountPercent;
    }
}

// Fixed: Configuration with private fields and validation
public final class SecureConfig {
    // Fixed: Private final fields
    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;
    }

    // Fixed: Getters provide read-only access
    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; }

    // Fixed: Builder pattern for controlled construction
    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) {
            // Fixed: Validate path
            if (!isValidPath(path)) {
                throw new IllegalArgumentException("Invalid config path");
            }
            this.configPath = path;
            return this;
        }

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

        private boolean isValidPath(String path) {
            // Validate path doesn't contain traversal
            return !path.contains("..") && path.startsWith("/etc/");
        }
    }
}
// Fixed: Secure applet with encapsulation
import java.applet.Applet;

public final class SecureApplet extends Applet {
    // Fixed: Private final fields
    private final String serverUrl;
    private final String apiKey;
    private boolean isAdmin;  // Controlled internally

    public SecureApplet() {
        // Fixed: Load from secure source
        this.serverUrl = loadServerUrl();
        this.apiKey = loadApiKey();
        this.isAdmin = false;  // Default to non-admin
    }

    // Fixed: No setters for critical fields
    // Admin status determined by authentication, not public field

    public void authenticate(String username, String password) {
        // Fixed: isAdmin set only through proper authentication
        AuthResult result = AuthService.authenticate(username, password);
        if (result.isSuccess()) {
            this.isAdmin = result.hasAdminRole();
        }
    }

    public boolean isAdmin() {
        return isAdmin;
    }

    private String loadServerUrl() {
        // Load from signed configuration
        return SignedConfig.get("server.url");
    }

    private String loadApiKey() {
        // Load from secure storage
        return SecureStorage.getApiKey();
    }
}

// Fixed: Immutable security context
public final class SecureSecurityContext {
    // Fixed: All fields private and 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;
    }

    // Fixed: Factory method for unauthenticated context
    public static SecureSecurityContext anonymous() {
        return new SecureSecurityContext(null, Collections.emptySet(), 0);
    }

    // Fixed: Factory method creates context from authentication result
    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;
        }
    }

    // Fixed: Getters only, no setters
    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;
    }
}
// Fixed: C++ class with proper encapsulation
class SecureAccount {
private:
    // Fixed: Private members with controlled access
    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) {}

    // Fixed: Controlled access through methods
    double getBalance() const { return balance_; }

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

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

    // Fixed: Lock/unlock requires authentication
    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) {
        // Log transaction for audit
    }
};

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the vulnerability pattern is well-documented in:

  • Java security guidelines
  • CERT Secure Coding Standards

References

  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."