finalize() Method Declared Public

Description

finalize() Method Declared Public is a security vulnerability in Java where a class declares its finalize() method with public access instead of the appropriate protected access. The finalize() method should only be called by the garbage collector during object reclamation or through super.finalize() calls within an overriding finalize implementation. A product should never call finalize() explicitly outside of these contexts. In mobile code or untrusted code environments, declaring finalize() as public allows malicious code to invoke the finalizer at will, potentially triggering premature resource cleanup, use-after-free conditions, or other security-sensitive operations at attacker-controlled times.

Risk

A public finalize() method creates significant security risks. Attackers can call finalize() on objects before they are garbage collected, causing premature cleanup of resources still in use. This can lead to use-after-free scenarios where resources appear valid but have been released. In security-sensitive contexts, explicit finalization calls can release locks, close connections, or clear security tokens at inappropriate times. Multiple calls to finalize() become possible since the attacker controls invocation. The method may be called during object construction through reflection, interfering with proper initialization. Any cleanup logic in finalize() becomes exploitable.

Solution

Always declare finalize() with protected access, never public. Better yet, avoid using finalize() altogether—it has been deprecated since Java 9. Use try-with-resources and AutoCloseable for resource management instead. If finalize() must be used, make it protected and ensure it handles multiple invocations safely. Consider using java.lang.ref.Cleaner (Java 9+) for cleanup of native resources. Never put security-critical operations in finalize() as the timing of execution is unpredictable even without malicious invocation.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Premature finalization may expose sensitive data that should have been cleared.
IntegrityScope: Integrity

Modify Application Data - Attacker-controlled finalization can corrupt object state or release resources prematurely.
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - Finalizing objects still in use can cause crashes or resource exhaustion.

Example Code

Vulnerable Code

// Vulnerable: finalize() declared public
import java.applet.Applet;

public final class VulnerableApplet extends Applet {
    private Connection dbConnection;
    private byte[] sensitiveData;

    @Override
    public void init() {
        dbConnection = openConnection();
        sensitiveData = loadSensitiveData();
    }

    // Vulnerable: Public finalize can be called by malicious code
    public void finalize() {
        // Cleanup resources
        if (dbConnection != null) {
            dbConnection.close();
            dbConnection = null;
        }

        // Clear sensitive data
        if (sensitiveData != null) {
            Arrays.fill(sensitiveData, (byte) 0);
            sensitiveData = null;
        }
    }

    public byte[] getSensitiveData() {
        return sensitiveData;  // May return null after malicious finalize()
    }

    public void useConnection() {
        dbConnection.query("SELECT * FROM data");  // NPE after finalize()
    }
}

// Attacker can exploit:
public class Attacker {
    public void exploit(VulnerableApplet applet) {
        // Force premature resource cleanup
        applet.finalize();

        // Now the applet has released its resources
        // but may still be referenced and used elsewhere

        // This will fail or expose corrupted state
        applet.useConnection();  // NullPointerException!
        applet.getSensitiveData();  // Returns null or stale data
    }
}

// Vulnerable: Security-sensitive finalize
public class VulnerableSecurityToken {
    private String token;
    private boolean isValid = true;

    public VulnerableSecurityToken(String token) {
        this.token = token;
    }

    // Vulnerable: Public access allows forced invalidation
    public void finalize() {
        invalidate();
    }

    private void invalidate() {
        token = null;
        isValid = false;
    }

    public boolean isValid() {
        return isValid && token != null;
    }

    public String getToken() {
        if (!isValid) {
            throw new SecurityException("Token invalidated");
        }
        return token;
    }
}

// Attack scenario
public class TokenAttacker {
    public void invalidateOtherUsersToken(VulnerableSecurityToken token) {
        // Malicious code can invalidate any token it can reference
        token.finalize();

        // Token is now invalid even though user didn't log out
        // token.isValid() returns false
    }
}

// Vulnerable: Resource manager with public finalize
public class VulnerableResourceManager {
    private FileHandle fileHandle;
    private Lock resourceLock;

    // Vulnerable: Public finalize releases resources
    public void finalize() {
        if (resourceLock != null && resourceLock.isHeldByCurrentThread()) {
            resourceLock.unlock();
        }

        if (fileHandle != null) {
            fileHandle.release();
            fileHandle = null;
        }
    }

    public void processFile() {
        resourceLock.lock();
        try {
            // Attacker could call finalize() from another thread here!
            // This would release the lock and file handle while in use
            fileHandle.read();
        } finally {
            resourceLock.unlock();
        }
    }
}

Fixed Code

// Fixed: finalize() declared protected (though deprecated)
import java.applet.Applet;

public final class SecureApplet extends Applet {
    private Connection dbConnection;
    private byte[] sensitiveData;

    @Override
    public void init() {
        dbConnection = openConnection();
        sensitiveData = loadSensitiveData();
    }

    // Fixed: Protected access - only GC or subclass can call
    @Override
    protected void finalize() throws Throwable {
        try {
            cleanup();
        } finally {
            super.finalize();
        }
    }

    private void cleanup() {
        if (dbConnection != null) {
            dbConnection.close();
            dbConnection = null;
        }

        if (sensitiveData != null) {
            Arrays.fill(sensitiveData, (byte) 0);
            sensitiveData = null;
        }
    }

    // Public cleanup method for explicit resource release
    public void close() {
        cleanup();
    }
}

// Better: Implement AutoCloseable, avoid finalize entirely
public class SecureResource implements AutoCloseable {
    private Connection dbConnection;
    private byte[] sensitiveData;
    private boolean closed = false;

    public SecureResource() {
        dbConnection = openConnection();
        sensitiveData = loadSensitiveData();
    }

    // Public close() is the proper way to release resources
    @Override
    public synchronized void close() {
        if (closed) return;

        if (dbConnection != null) {
            dbConnection.close();
            dbConnection = null;
        }

        if (sensitiveData != null) {
            Arrays.fill(sensitiveData, (byte) 0);
            sensitiveData = null;
        }

        closed = true;
    }

    public byte[] getSensitiveData() {
        checkNotClosed();
        return sensitiveData.clone();  // Return copy
    }

    private void checkNotClosed() {
        if (closed) {
            throw new IllegalStateException("Resource has been closed");
        }
    }
}

// Usage with try-with-resources
public class SecureUsage {
    public void process() {
        try (SecureResource resource = new SecureResource()) {
            byte[] data = resource.getSensitiveData();
            // Use data
        }
        // Automatically closed
    }
}

// Fixed: Security token without finalize
public class SecureSecurityToken implements AutoCloseable {
    private volatile String token;
    private volatile boolean isValid = true;
    private final Object lock = new Object();

    public SecureSecurityToken(String token) {
        this.token = token;
    }

    // No public finalize - use close() instead
    @Override
    public void close() {
        synchronized (lock) {
            token = null;
            isValid = false;
        }
    }

    public boolean isValid() {
        synchronized (lock) {
            return isValid && token != null;
        }
    }

    public String getToken() {
        synchronized (lock) {
            if (!isValid) {
                throw new SecurityException("Token invalidated");
            }
            return token;
        }
    }
}

// Fixed: Using Cleaner for native resources (Java 9+)
import java.lang.ref.Cleaner;

public class SecureNativeResource implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    private final long nativeHandle;
    private final Cleaner.Cleanable cleanable;

    public SecureNativeResource() {
        this.nativeHandle = allocateNative();

        // Register cleanup action - runs when object becomes phantom reachable
        final long handle = this.nativeHandle;
        this.cleanable = cleaner.register(this, () -> {
            freeNative(handle);
        });
    }

    @Override
    public void close() {
        // Explicit cleanup
        cleanable.clean();
    }

    private static native long allocateNative();
    private static native void freeNative(long handle);

    // No finalize() needed - Cleaner handles it safely
}

// Fixed: Thread-safe resource manager
public class SecureResourceManager implements AutoCloseable {
    private FileHandle fileHandle;
    private final ReentrantLock resourceLock = new ReentrantLock();
    private volatile boolean closed = false;

    @Override
    public void close() {
        resourceLock.lock();
        try {
            if (!closed) {
                if (fileHandle != null) {
                    fileHandle.release();
                    fileHandle = null;
                }
                closed = true;
            }
        } finally {
            resourceLock.unlock();
        }
    }

    public void processFile() {
        resourceLock.lock();
        try {
            if (closed) {
                throw new IllegalStateException("Manager is closed");
            }
            // Safe - lock protects against concurrent close
            fileHandle.read();
        } finally {
            resourceLock.unlock();
        }
    }
}

// Note: If you must use finalize, this is the minimal safe pattern
public class MinimalFinalize {
    private Resource resource;

    // Protected, handles multiple calls, calls super
    @Override
    protected void finalize() throws Throwable {
        try {
            if (resource != null) {
                resource.release();
                resource = null;
            }
        } finally {
            super.finalize();
        }
    }
}

CVE Examples

No specific CVEs are commonly attributed to this CWE directly, though the vulnerability pattern is recognized in mobile code and applet security contexts.


References

  1. MITRE Corporation. "CWE-583: finalize() Method Declared Public." https://cwe.mitre.org/data/definitions/583.html
  2. Joshua Bloch. "Effective Java" - Item 8: Avoid finalizers and cleaners.
  3. Oracle. "Object.finalize() - Deprecated since Java 9."