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
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Modify Application Data - The object could potentially be tampered with, allowing attackers to modify critical values that affect program behavior. |
| Confidentiality | Scope: Confidentiality Read Application Data - The object could potentially be read, allowing attackers to discover sensitive configuration or state information. |
| Access Control | Scope: 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
- MITRE Corporation. "CWE-493: Critical Public Variable Without Final Modifier." https://cwe.mitre.org/data/definitions/493.html
- CERT Oracle Secure Coding Standard for Java. "OBJ10-J. Do not use public static nonfinal fields."
- Oracle. "Secure Coding Guidelines for Java SE."