Untrusted code can misuse APIs provided by trusted code by overriding to override methods such as Object.equals()
, Object.hashCode()
, and Thread.run()
. These methods are primarily targeted valuable targets because they are most often commonly used behind the scenes and may interact with components in a way that is not clearly easily discernible.
By providing overridden implementations, an attacker can use untrusted code may be able to glean sensitive information, cause run arbitrary code to run and expose denial of service vulnerabilities, or launch a denial of service attack.
See MET52-J. Do not use the clone() method to copy untrusted method parameters for more specific details regarding overriding the Object.clone()
method.
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.
...
The constructor for LicenseManager
initializes licenseMap
with a demo license key which is meant to be kept that must remain secret. The license key is hardcoded hard-coded for illustrative purposes and ; it should ideally be read from an external configuration file that stores its an encrypted version of the key. The LicenseType
class provides overridden implementations of equals()
and hashCode()
methods.
This setup can expose the demo license key if implementation is vulnerable to an attacker who extends the LicenseType
class as follows and overrides the equals()
and hashCode()
methods.:
Code Block |
---|
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 following is the malicious client program is shown below.:
Code Block |
---|
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) { // prints ABC-DEF-PQR-XYZ 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 can discover the sensitive data present within the licenseMap
. That is possible by facilitating in only a few minutes. The attack operates by discovering at least one hash collision with respect to the key of the map.
Noncompliant Code Example
How many items are there in layouts in the end?
Compliant Solution (IdentityHashMap
)
This compliant solution uses an IdentityHashMap
rather than a HashMap
to store the license information:
Code Block | ||
---|---|---|
| ||
Code Block | ||
public class WidgetLicenseManager { privateMap<LicenseType, intString> noOfComponents; licenseMap = new IdentityHashMap<LicenseType, public Widget(int noOfComponents) {String>(); this.noOfComponents = noOfComponents; }// ... } |
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 anIdentityHashMap
, two keysk1
andk2
are considered equal if and only if(k1==k2)
. (In normalMap
implementations (likeHashMap
) two keysk1
andk2
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 can even retrieve the added key-value pairs, as demonstrated by the following client code.
Code Block |
---|
public class DemoClient { public static intvoid getNoOfComponentsmain(String[] args) { returnLicenseManager noOfComponents; licenseManager = } public void setNoOfComponents(int noOfComponents) { new LicenseManager(); LicenseType this.noOfComponentstype = noOfComponentsnew LicenseType(); } // Also overrides hashCode() (code is omitted) ... type.setType("custom-license-key"); public boolean equals(Object o) {licenseManager.setLicenseKey(type, "CUS-TOM-LIC-KEY"); ifObject (olicenseKeyValue == null || !(o instanceof Widget)) { licenseManager.getLicenseKey(type); // Prints CUS-TOM-LIC-KEY return false System.out.println(licenseKeyValue); } } |
Compliant Solution (final
Class)
This compliant solution declares the LicenseType
class final so that its methods cannot be overridden:
Code Block | ||
---|---|---|
| ||
final class LicenseType { } // ... } |
Noncompliant Code Example
This noncompliant code example consists of a Widget
class and a LayoutManager
class containing a set of widgets:
Code Block | ||
---|---|---|
| ||
public class Widget { private int noOfComponents; public Widget(int noOfComponents check for negative components if (noOfComponents < 0) { this.noOfComponents = noOfComponents; } public int return false;getNoOfComponents() { return noOfComponents; } public final void Widget widget = (Widget) o; setNoOfComponents(int noOfComponents) { return this.noOfComponents == ((Widget) o).getNoOfComponents() noOfComponents; } } } public classboolean Navigator extends Widget equals(Object o) { public Navigator(int noOfComponentsif (o == null || !(o instanceof Widget)) { return super(noOfComponents)false; } @Override Widget widget = public boolean equals(Object o) {(Widget) o; return if (o this.noOfComponents == null) {widget.getNoOfComponents(); } @Override public int hashCode() { int res return= false31; res = res * } 17 + noOfComponents; return trueres; } } 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(); } } |
Client code
An attacker can extend the Widget
class as a Navigator
widget and override the hashCode()
method:
Code Block |
---|
public class Navigator extends Widget {
public Navigator(int noOfComponents) {
super(noOfComponents);
}
@Override
public int hashCode() { |
Code Block |
Widget nav = new Navigator(-1); int res = 31; Widget widgetres = res new Widget(10)* 17; return res; } } |
The client code follows:
Code Block |
---|
Widget nav LayoutManager manager = new LayoutManagerNavigator(1); Widget widget = new Widget(1); LayoutManager manager = new LayoutManager(); manager.addWidget(nav); manager.addWidget(widget); System.out.println(manager.getLayoutSize()); // prints 2 |
Noncompliant Code Example
What gets printed - main or child / both / either ?
Prints 2 |
The set layouts
is expected to contain just one item because the number of components for both the navigator and widget being added is 1. However, the getLayoutSize()
method returns 2.
The reason for this discrepancy is that the hashCode()
method of Widget
is used only once when the widget is added to the set. When the navigator is added, the hashCode()
method provided by the Navigator
class is used. Consequently, the set contains two different object instances.
Compliant Solution (final
Class)
This compliant solution declares the Widget
class final so that its methods cannot be overridden:
Code Block | ||
---|---|---|
| ||
public final class Widget {
// ...
} |
Noncompliant Code Example (run()
)
In this noncompliant code example, class Worker
and its subclass SubWorker
each contain a startThread()
method intended to start a thread:
Code Block | ||
---|---|---|
| ||
public class Worker | ||
Code Block | ||
public class Trusted implements Runnable { TrustedWorker() { } public void startThread(String name) { new Thread(this, name).start(); } @Override public void run() { System.out.println("ChildParent"); } } public class UntrustedSubWorker extends TrustedWorker { // Note untrusted code may start a new thread even during construction @Override public void UntrustedstartThread(String name) { super.startThread(name); new Thread(this, name).start(); } } @Override public void run() { System.out.println("Child"); } } |
If a client runs the following code:
Code Block |
---|
Worker w = System.out.println("Main"); } } |
Client code:
...
new SubWorker();
w.startThread("thread"); |
the client may expect Parent
and Child
to be printed. However, Child
is printed twice because the overridden method run()
is invoked both times that a new thread is started.
Compliant Solution
This compliant solution modifies the SubWorker
class and removes the call to super.startThread()
:
Code Block | ||
---|---|---|
| ||
public class SubWorker extends Worker {
@Override
public void startThread(String name) {
new Thread(this, name).start();
}
// ...
} |
The client code is also modified to start the parent and child threads separately. This program produces the expected output:
Code Block |
---|
Worker w1 = new Worker();
w1.startThread("parent-thread");
Worker w2 = new SubWorker();
w2.startThread("child-thread"); |
Bibliography
[API 2013] | Class IdentityHashMap |
[Hawtin 2006] | [drlvm][kernel_classes] ThreadLocal vulnerability |
...