Callbacks provide a means to register a method to be invoked or called back when an interesting event occurs. Java uses callbacks for applet and servlet life cycle events, AWT and Swing event notifications such as button clicks, and asynchronously reading and writing data from storage and even in Runnable.run()
wherein a new thread automatically executes the specified run()
method.
In Java, callbacks are typically implemented using interfaces. The general structure of a callback is as follows.
public interface CallBack { void callMethod(); } class MyCallBack implements CallBack { public void callMethod() { System.out.println("CallBack invoked"); } } class Client { CallBack callback; public void registerCallBack(CallBack callback) { this.callback = callback; } public void doSomething() { callback.callMethod(); } public static void main(String[] args) { Client client = new Client(); client.registerCallBack(new MyCallBack()); // ... client.doSomething(); // prints "CallBack invoked" } }
Callback methods are often invoked with no changes in privileges. This means that they may be executed in a context that has more privileges than the context in which they are declared. If these callback methods accept data from untrusted code, privilege escalation may occur.
According to Oracle's Secure Coding Guidelines [SCG 2010]:
Callback methods are generally invoked from the system with full permissions. It seems reasonable to expect that malicious code needs to be on the stack in order to perform an operation, but that is not the case. Malicious code may set up objects that bridge the callback to a security checked operation. For instance, a file chooser dialog box that can manipulate the filesystem from user actions, may have events posted from malicious code. Alternatively, malicious code can disguise a file chooser as something benign while redirecting user events.
Noncompliant Code Example
This noncompliant code example uses a CallBackImpl
class that implements the CallBack
interface. The securityCritical()
method accepts a privileged action that is used by its implementation as an argument to a doPrivileged
block.
public interface CallBack { void securityCritical(PrivilegedAction<String> action); } class CallBackImpl implements CallBack { public void securityCritical(PrivilegedAction<String> action) { AccessController.doPrivileged(action); } } class Client { public void register(CallBack callback) { callback.securityCritical(new MaliciousUserLookupAction(7)); } public static void main(String[] args) { Client client = new Client(); CallBack callBack = new CallBackImpl(); client.register(callBack); } }
The class Client
allows registering the callback so that an untrusted caller can specify the user id to look-up. An object of type UserLookupAction
is expected by the callback, however, an attacker may extend that class and replace it with a malicious implementation in the form of a MaliciousUserAction
instance.
public class UserLookupAction implements PrivilegedAction<String> { private int userid; public UserLookupAction(int userid) { this.userid = userid; } public String run() { String name = null; try (InputStream fis = new FileInputStream("/etc/passwd")) { // Look up userid & assign to name } catch (IOException x) { name = null; } return name; } } class MaliciousUserLookupAction extends UserLookupAction { public MaliciousUserLookupAction(int userid) { super(userid); } public String run() { System.out.println("Executing untrusted code"); return null; } }
Consequently, the malicious code will execute with privileges of the class CallBackImpl
that defines the callback in securityCritical()
method.
Compliant Solution
According to Oracle's secure coding guidelines [SCG 2010]:
By convention, instances of
PrivilegedAction
andPrivilegedExceptionAction
may be made available to untrusted code, butdoPrivileged
must not be invoked with caller-provided actions.
This compliant solution amends the CallBack
interface and instead of accepting the PrivilegedAction
objects, the securityCritical()
methods accepts the user id to be searched for. The code contained within the UserLookupAction
class is moved to the securityCritical()
method.
public interface CallBack { void securityCritical(int uid); } class CallBackImpl implements CallBack { public void securityCritical(int uid) { AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { String name; try (InputStream fis = new FileInputStream("/etc/passwd")) { // Look up userid & assign to name } catch (IOException x) { name = null; } return name; } }); } } class Client { public void register(CallBack callback) { callback.securityCritical(7); } public static void main(String[] args) { Client client = new Client(); CallBack callBack = new CallBackImpl(); client.register(callBack); } }
Class UserLookupAction
is no longer required. This solution does not allow untrusted code to supply arbitrary statements for execution.
Applicability
Exposing sensitive methods through callbacks can result in misuse of privileges and arbitrary code execution.
Bibliography
[API 2011] | |
[SCG 2010] | Guideline 9-3: Safely invoke Guideline 9-2: Beware of callback methods |