
Introduction
Sometimes, not always, but sometimes, the most interesting security discoveries start with someone trying to prove you wrong on social media.
This is what happened when a user questioned one of our vulnerability reports about a collection library’s implementation of unique lists. What started as a simple Stack Overflow error led us down the rabbit hole and revealed a security flaw affecting Java’s core collection objects, like java.util
.
We named this SkibidiJava
as a nod to how Java’s automatic method calls can create bizarrely dangerous behaviors in complex environments, turning normal and naive operations into, well, unexpected skibidi chaos.
While this investigation revealed fascinating insights into Java’s core behaviors, it remains in the realm of theoretical research–an intriguing demonstration of how language features can interact in unexpected ways–rather than an observed security vulnerability.
The Initial Discovery
It began with a reported StackOverflowError
error in Apache Common Collections’ unique list implementation when the list receives itself as an argument, creating a circular reference. While this might sound like a theoretical edge case requiring deep access to code execution, our investigation revealed it could be weaponized through a much more common attack vector: Java serialization.
The Basic Flaw
The original flaw manifests when a unique list implementation contains a reference to itself, causing infinite recursion during operations like hashCode()
. When collections are used as keys in a HashMap, Java must call hashCode()
to determine their bucket location – this is a fundamental part of how HashMaps works.
At a glance, it might seem like an unlikely scenario unless we let somebody who wanted to crash our application write it for us. However, attempting to deserialize a HashMap containing our circular-referenced collection as a key will automatically trigger the hashCode()
calculation, leading to the StackOverflowError
.
Here’s a simple demonstration:
List<Object> list = UniqueList.create(new ArrayList<>());
list.add(list); // This creates a circular reference
list.hashCode(); // This triggers a StackOverflowError
And now, a slightly “sophisticated” exploit:
public static void createExploit() throws Exception {
ArrayList<Object> al = new ArrayList<Object>();
HashMap<Object, String> map = new HashMap<Object, String>();
map.put(al, "oops");
al.add(al);
// Serialize
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
// Deserialize (this will crash)
ByteArrayInputStream bais = new
ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject(); // StackOverflowError
}
Going Deeper: A Universal Java DoS
What started as an investigation into a specific collection library led us to believe this wasn’t just about unique lists. The root cause lies in how Java’s core collection classes, java.util
, handle recursive operations. Due to the inheritance patterns between Collection and ArrayList, operations that traverse collection elements can enter an infinite recursion loop when circular references are present.
While many secure applications and frameworks typically restrict deserialization of java.util
classes, the fundamental nature of these core Java collections means that homegrown or customized deserializers might allow them, making this flaw particularly concerning.
Most importantly, this manifests as a StackOverflowError
rather than an Exception
– a critical distinction in Java’s error hierarchy. While exceptions can be caught with `catch Exception` blocks, Error
objects bypass these handlers. Although technically catchable with a catch Error
, this is rarely implemented, as Errors represent unrecoverable JVM conditions that shouldn’t be caught, making this an effective vector for DoS attacks.
The Universal Gadget
We discovered that this simple pattern could crash virtually any Java deserializer that handles standard collection types:
ArrayList<Object> al = new ArrayList<Object>();
HashMap<Object, String> map = new HashMap<Object, String>();
map.put(al, "oops");
al.add(al);
When this structure is serialized and then deserialized, it triggers an infinite recursion between ArrayList and Collection, causing a Denial of Service through stack overflow error.
Weaponizing the Weakness
Real-World Impact
This weakness, manifesting as a vulnerability, becomes particularly dangerous in systems that deserialize untrusted user-provided data into objects without proper validation. While Java often chooses to face deserialization vulnerabilities by allowing certain namespaces, since this particular issue is baked into java.util
classes, it could be accidentally allowed.
Common vulnerable patterns include:
- Message queues that deserialize Java objects
- Remote method invocation services
- APIs that deserialize JSON to Java objects
- Distributed cache systems
- Any framework that deserializes untrusted data
Mitigation Strategies
The best mitigation approach will depend on the specific use case, but here are some general strategies:
- Input Validation: Implement strict validation of serialized data before deserialization
- Custom Deserializers: Use custom deserializers that can detect and prevent circular references by limiting object depth and validating; this depth should be tailored for need.
- Allowed Objects: Maintain a strict allowlist of permitted classes for deserialization.
Sample Mitigation Code
While the following code demonstrates circular reference detection, note that a complete secure deserialization implementation would also require a secure resolveClass
method with proper class allowlisting to prevent standard Java deserialization attacks.
public class SafeObjectInputStream extends ObjectInputStream {
private int maxDepth = 100; // Configurable max depth
private Set<Object> seen = new HashSet<>();
protected Object resolveObject(Object obj) throws IOException {
if (seen.size() > maxDepth) {
throw new SecurityException("Potential circular reference detected");
}
seen.add(obj);
return super.resolveObject(obj);
}
}
Lessons Learned
This investigation into Java’s collection classes highlights several key principles for security researchers:
- Even well-established libraries can contain simple but significant security flaws
- “Theoretical” edge-case vulnerabilities can have practical exploit paths
- Core language features need the same security scrutiny as application code
- Don’t dismiss edge cases – they might reveal deeper systemic issues
Conclusion
This journey from a “minor” edge case to discovering a universal Java DoS-prone behavior highlights the importance of thorough security research and understanding inheritance patterns in core language features. While the original collection library issue seemed limited in scope, it led us to uncover a more fundamental problem in Java’s collection hierarchy