I'm reading Effective Java by Joshua Bloch. In ITEM 8: AVOID FINALIZERS AND CLEANERS of CHAPTER 2 he states:
Finalizers have a serious security problem: they open your class up to finalizer attacks.The idea behind a finalizer attack is simple: If an exception is thrown from a constructor or its serialization equivalents—the
readObject
andreadResolve
methods (Chapter 12)—the finalizer of a malicious subclass can run on the partially constructed object that should have “died on the vine.” This finalizer can record a reference to the object in a static field, preventing it from being garbage collected. Once the malformed object has been recorded, it is a simple matter to invoke arbitrary methods on this object that should never have been allowed to exist in the first place. Throwing an exception from a constructor should be sufficient to prevent an object from coming into existence; in the presence of finalizers, it is not. Such attacks can have dire consequences. Final classes are immune to finalizer attacks because no one can write a malicious subclass of a final class.
Firstly, I know finalizers have been deprecated since Java 18. Nevertheless, I think it's important to understand the reason behind this decision. My understanding of the excerpt above is as follows:
And secondly, I hope my conceptual understanding of the issue is correct. However, Bloch hasn't demonstrated this issue in a tangible code example. Perhaps because he doesn't want us to mess around with the finalize mechanism in Object
.
Could you please demonstrate this to me in code?
For instance, if I have a superclass:
/** Superclass */
public class DemoSecurityProblem {
}
And then the subclass either by inheritance or composition:
public class MaliciousSubClass extends DemoSecurityProblem {
DemoSecurityProblem demoSecurityProblem = new DemoSecurityProblem();
}
How can an attacker exploit this via the finalize mechanism?
Thanks a lot!
Your description is basically correct, but overcomplicating things. There is no need to store something in a static
variable; as soon as the finalize()
method is invoked, the object is already resurrected, as invoking a method on an object implies invoking code with access to the object.
Storing the object reference in a variable is a way to expand the lifetime beyond the execution of the finalize()
method but this is not a necessary thing for the attack. Also, instead of using a static
variable, the attacker could also make the subclass an inner class and store the reference in the still reachable outer object.
So the following program is already enough to demonstrate the issue
public class FinalizerAttackExample {
public static void main(String[] args) throws InterruptedException {
try {
new MaliciousSubclass();
} catch(SecurityException ex) {
System.out.println("wouldn't get hands on a ResourceClass instance");
}
System.gc();
Thread.sleep(2000);
}
static class ResourceClass {
ResourceClass() {
if(!checkCaller()) throw new SecurityException();
}
public void criticalAction() {
System.out.println("ResourceClass.criticalAction()");
}
}
/** For our demonstration, all callers are invalid */
static boolean checkCaller() {
return false;
}
static class MaliciousSubclass extends ResourceClass {
@Override
protected void finalize() {
System.out.println("see, I got hands on " + this);
criticalAction();
}
}
}
While garbage collection is non-deterministic and the execution of finalizers not guaranteed in general, this example will print
wouldn't get hands on a ResourceClass instance
see, I got hands on FinalizerAttackExample$MaliciousSubclass@7ad74083
ResourceClass.criticalAction()
on a lot of implementations, demonstrating that criticalAction()
could be invoked on an object that shouldn’t exist as the constructor threw an exception.