Untrusted code can misuse APIs provided by trusted code by overriding methods such as Object.equals()
, Object.hashCode()
and Thread.run()
. These methods are primarily targeted because they are most often used behind the scenes and may interact with components in a way that is not clearly discernible.
By providing overridden implementations, untrusted code may be able to glean sensitive information, cause arbitrary code to run and expose denial of service vulnerabilities.
Noncompliant Code Example (hashCode)
This noncompliant code example shows a LicenseManager
class that maintains a licenseMap
. The map stores a LicenseType
and license value pair.
public class LicenseManager { Map<LicenseType, String> licenseMap = new HashMap<LicenseType, String>(); public LicenseManager() { LicenseType type = new LicenseType(); type.setType("demo-license-key"); licenseMap.put(type, "ABC-DEF-PQR-XYZ"); } public Object getLicenseKey(LicenseType licenseType) { return licenseMap.get(licenseType); } public void setLicenseKey(LicenseType licenseType, String licenseKey) { licenseMap.put(licenseType, licenseKey); } } class LicenseType { private String type; public String getType() { return type; } public void setType(String type) { this.type = type; } @Override public int hashCode() { int res = 17; res = res * 31 + type == null ? 0 : type.hashCode(); return res; } @Override public boolean equals(Object arg) { if (arg == null || !(arg instanceof LicenseType)) { return false; } if (type.equals(((LicenseType) arg).getType())) { return true; } return false; } }
The constructor for LicenseManager
initializes licenseMap
with a demo license key which is meant to be kept secret. The license key is hardcoded for illustrative purposes and should ideally be read from an external configuration file that stores its encrypted version. The LicenseType
class provides overridden implementations of equals()
and hashCode()
methods.
This setup can expose the demo license key if an attacker extends the LicenseType
class as follows and overrides the equals()
and hashCode()
methods.
public class CraftedLicenseType extends LicenseType { private static int guessedHashCode = 0; @Override public int hashCode() { // Returns a new hashCode to test every time get() is called guessedHashCode++; return guessedHashCode; } @Override public boolean equals(Object arg) { // Always returns true return true; } }
The client program is shown below.
public class DemoClient { public static void main(String[] args) { LicenseManager licenseManager = new LicenseManager(); for (int i = 0; i <= Integer.MAX_VALUE; i++) { Object guessed = licenseManager .getLicenseKey(new CraftedLicenseType()); if (guessed != null) { System.out.println(guessed); } } } }
The client program runs through the sequence of all possible hash codes using CraftedLicenseType
until it successfully matches the hash code of the demo license key object stored in the LicenseManager
class. Consequently, within a few minutes the attacker is able to find the sensitive data present within the licenseMap
. That is possible by facilitating at least one hash collision with respect to the key of the map.
Compliant Solution
This compliant solution uses an IdentityHashMap
instead of HashMap
to store the license information.
public class LicenseManager { Map<LicenseType, String> licenseMap = new IdentityHashMap<LicenseType, String>(); // ... }
According to the Java API class IdentityHashMap
documentation [API 2006]
This class implements the Map interface with a hash table, using reference-equality in place of object-equality when comparing keys (and values). In other words, in an IdentityHashMap, two keys k1 and k2 are considered equal if and only if (k1==k2). (In normal Map implementations (like HashMap) two keys k1 and k2 are considered equal if and only if (k1==null ? k2==null : k1.equals(k2)).)
Consequently, the overridden methods cannot expose internal class details. The client program can continue to add license keys and even retrieve the added key-value pairs as demonstrated by the following client code.
public class DemoClient { public static void main(String[] args) { LicenseManager licenseManager = new LicenseManager(); LicenseType type = new LicenseType(); type.setType("custom-license-key"); licenseManager.setLicenseKey(type, "CUS-TOM-LIC-KEY"); Object licenseKeyValue = licenseManager.getLicenseKey(type); System.out.println(licenseKeyValue); } }
Compliant Solution (final class)
This compliant solution declares the LicenseType
class final so that its methods cannot be overridden.
final class LicenseType { // ... }
Noncompliant Code Example (equals())
This noncompliant code example consists of a Widget
class that attempts to mandate that comparison of a widget having negative number of components with another yields a false result.
public class Widget { private int noOfComponents; public Widget(int noOfComponents) { this.noOfComponents = noOfComponents; } public int getNoOfComponents() { return noOfComponents; } public void setNoOfComponents(int noOfComponents) { this.noOfComponents = noOfComponents; } // Also overrides hashCode() (code is omitted) ... public boolean equals(Object o) { if (o == null || !(o instanceof Widget)) { return false; } Widget widget = (Widget) o; // check for negative components if (noOfComponents < 0 || widget.getNoOfComponents() < 0) { return false; } return this.noOfComponents == widget.getNoOfComponents(); } } public class LayoutManager { private Set<Widget> layouts = new HashSet<Widget>(); public void addWidget(Widget widget) { if (!layouts.contains(widget)) { layouts.add(widget); } } public int getLayoutSize() { return layouts.size(); } }
A LayoutManager
class containing a set of widgets is used by the example. An attacker can extend the Widget
class as a Navigator
widget and override the equals()
method.
public class Navigator extends Widget { public Navigator(int noOfComponents) { super(noOfComponents); } @Override public boolean equals(Object o) { // Always returns true return true; } }
A navigator having negative number of components and a widget having nonnegative number of components are added to the layout manager class's layouts
set. It is expected that the set would contain only one widget, however, the getLayoutSize()
method returns two.
Client code
Widget nav = new Navigator(-1); Widget widget = new Widget(10); LayoutManager manager = new LayoutManager(); manager.addWidget(nav); manager.addWidget(widget); System.out.println(manager.getLayoutSize()); // prints 2
The reason for this discrepancy is that the equals()
method of Widget
is not used; instead the equals()
method provided by the Navigator
class is used.
Noncompliant Code Example (run())
What gets printed - main or child / both / either ?
public class Trusted implements Runnable { Trusted() { } public void startThread(String name) { new Thread(this, name).start(); } @Override public void run() { System.out.println("Child"); } } public class Untrusted extends Trusted { // Note untrusted code may start a new thread even during construction Untrusted(String name) { super.startThread(name); } @Override public void run() { System.out.println("Main"); } }
Client code:
Trusted a = new Untrusted("Main"); a.run();