Lab 05 — JVM Unsafe Deserialization: Break, Fix, Defend

Language: Java 17+ | Tools: javac, java | Difficulty: 4/5

Pairs with the Phase 02 WARMUP Chapter 11 (insecure deserialization). Connects to the CVE Casebook (Log4Shell).

Why This Lab Exists (Purpose & Goal)

Insecure deserialization is the bug class that turns "the server received some data" into "the server ran the attacker's code," and it has produced a long line of critical CVEs across the JVM ecosystem (and Python pickle, .NET, Ruby, PHP). It is dangerous precisely because it looks innocuous — you're "just loading an object." The goal of this lab is to see the execution boundary with your own eyes (safely), understand why it exists, and then build the structural fix: refuse native serialized streams from untrusted sources entirely.

The Concept, In the Weeds

ObjectInputStream.readObject() does not just copy data — it reconstructs an arbitrary object graph, and during reconstruction the JVM invokes class-controlled hooks (readObject, readResolve, readExternal, finalizers). The included LabCallback is harmless — it only flips an in-memory flag — but that flag flip proves the execution boundary: code ran during deserialization, controlled by the type of the incoming bytes, without you calling it. In the real world, attackers chain these hooks across classes already on the classpath (a gadget chain) into remote code execution; this lab demonstrates the mechanism without shipping a weaponized chain.

The defenses, in order of strength:

  • A class allowlist is partial defense, not permission. It narrows which types can be instantiated, but it does not make accepting an untrusted native serialized stream safe — new gadgets keep appearing in dependencies.
  • The structural fix: don't accept native serialization from untrusted sources. Parse a small, length-bounded, schema-defined text/binary envelope (JSON/protobuf) and construct your domain objects explicitly. Data should never get to choose behavior.
  • Bound before you parse — bytes, fields, nesting, collection sizes, and total work — to stop resource-exhaustion DoS.

The broader lesson (the case study's point, and Log4Shell's): data that reaches a powerful interpretation mechanism can cross an unexpected trust boundary. Log4Shell was a logging string reaching a JNDI lookup; deserialization is bytes reaching an object-graph constructor. The pattern is the same.

Why This Matters for Protecting the Company

A single endpoint that deserializes untrusted input is often a pre-authentication remote code execution — the most severe finding possible — and these endpoints hide in surprising places (caches, session stores, message queues, inter-service calls). When you can recognize the danger of native deserialization, inventory where it happens, and migrate to schema-validated formats with explicit object construction, you close one of the highest-impact attack classes across the company's JVM (and equivalent) services. You also learn to resist the seductive-but-incomplete "we added a class allowlist" half-fix.

Run

make test

Build / Lessons

  • A class allowlist is partial defense, not a reason to accept untrusted native serialized streams.
  • Prefer simple, schema-defined formats and construct domain objects explicitly.
  • Bound bytes, fields, nesting, collections, and work before parsing.
  • Isolate legacy deserialization, authenticate the source, remove gadget-rich dependencies, log rejections, and migrate the protocol.

Validation — What You Should Be Able to Do Now

  • Explain why readObject() can execute code — that deserialization reconstructs an object graph and invokes type-controlled hooks — without needing a real gadget chain to make the point.
  • Explain why a class allowlist is a mitigation, not a license, and what the structural fix is.
  • Generalize the lesson to Log4Shell / JNDI / pickle: untrusted data reaching a powerful interpreter crosses a trust boundary.
  • Inventory the non-obvious places deserialization hides (caches, queues, sessions, RPC).

The Broader Perspective

This lab teaches the single most generalizable rule in injection security, the same one behind SQL injection, command injection, XSS, and template injection: never let untrusted data control code/behavior; keep a hard separation, and prefer simple data formats over powerful interpreters. Once you see that "deserialize," "evaluate," "log-with-lookup," and "concatenate into a query" are all the same mistake — handing data to something that interprets it — you can spot the bug class anywhere, in any language, even in formats nobody calls "deserialization." That pattern-recognition is what makes a senior application-security reviewer fast and accurate.

Interview Angle

  • "Why is insecure deserialization dangerous, and how do you fix it?" — Native deserialization rebuilds arbitrary object graphs and invokes type-controlled hooks that can be chained into RCE. The fix is structural: don't deserialize untrusted native objects; use schema-validated data formats with explicit construction, bound resources, and isolate any legacy deserialization.

References

  • Phase 02 WARMUP Chapter 11; OWASP Deserialization Cheat Sheet.
  • The CVE Casebook (Log4Shell, CVE-2021-44228).