What is the 'look-ahead deserialization' pattern, and how would you implement it without JEP 290?
answer
- look-ahead = check class name BEFORE instantiation
- override ObjectInputStream.resolveClass(desc) -> desc.getName()
- throw InvalidClassException for non-allowlisted
- also resolveProxyClass + array element normalization
- JEP 290 ObjectInputFilter = same idea, configurable; Commons-IO ValidatingObjectInputStream
basics
~20 sLook-ahead deserialization means checking the name of each class in the stream before that class is actually created, and refusing any class you didn't expect. Before JEP 290 you did this by subclassing ObjectInputStream and overriding resolveClass to validate the class name and throw if it's not allowed.
solid answer
~40 sThe deserialization attack works because gadget classes are instantiated and their hook methods run during reconstruction. 'Look-ahead' flips the timing: inspect the class descriptor *before* the object is built and reject disallowed classes early. Pre-Java-9, you subclass `ObjectInputStream` and override `resolveClass(ObjectStreamClass desc)` (and `resolveProxyClass` for dynamic proxies). The descriptor exposes `desc.getName()`; you check it against an allowlist and throw `InvalidClassException` for anything else, before `super.resolveClass` loads it. This is the same idea JEP 290 later standardized as `ObjectInputFilter` — the filter runs at the same look-ahead point but is configurable without subclassing. Use an **allowlist**, not a blocklist, and remember to handle arrays/proxies. Apache's `ValidatingObjectInputStream` (commons-io) is a ready-made implementation. It's defense in depth; the strongest fix is still avoiding native deserialization of untrusted input.
code
java · 17 lines// Pre-JEP-290 look-ahead: reject classes before they are instantiated.
class SafeOIS extends ObjectInputStream {
private static final Set<String> ALLOWED =
Set.of("com.example.dto.User", "java.util.ArrayList");
SafeOIS(InputStream in) throws IOException { super(in); }
@Override protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String base = desc.getName().replaceAll("^\\[+L?", "").replace(";", "");
if (!ALLOWED.contains(base))
throw new InvalidClassException("Blocked", desc.getName());
return super.resolveClass(desc);
}
@Override protected Class<?> resolveProxyClass(String[] ifaces)
throws IOException, ClassNotFoundException {
throw new InvalidClassException("No proxies allowed");
}
}go deeper
Understands the idea of checking a class before it's created and rejecting unexpected ones.
Can override resolveClass to allowlist class names and throw InvalidClassException for the rest.
Implements robust look-ahead (arrays + resolveProxyClass + allowlist), explains its timing vs the attack, and relates it to JEP 290 / ValidatingObjectInputStream.
Decides between hand-rolled look-ahead and JEP 290 across a heterogeneous estate, accounts for legacy runtimes, and positions it as one layer beneath migrating off native serialization.
## The timing problem Recall why native deserialization is dangerous: `ObjectInputStream.readObject()` reads class names from the *stream*, loads and instantiates those classes, and runs their magic hook methods (`readObject`, `readResolve`, `readExternal`) **during** reconstruction. So by the time control returns to your code (and your cast runs), any gadget chain has already executed. The defensive principle is therefore about **timing**: you must reject a bad class *before* it is instantiated. That early check is **look-ahead deserialization** — you 'look ahead' at the class descriptor in the stream before building the object. ## The pre-JEP-290 implementation: override `resolveClass` `ObjectInputStream` has a protected method called once per class in the stream, *before* the class's instances are created: ```java protected Class<?> resolveClass(ObjectStreamClass desc) ``` The argument `desc` is an `ObjectStreamClass` — metadata about the class about to be deserialized, crucially exposing `desc.getName()` (the fully-qualified class name). The default implementation just loads that class. By **subclassing** `ObjectInputStream` and overriding `resolveClass`, you can validate the name first and throw `java.io.InvalidClassException` for anything not on your allowlist, *before* `super.resolveClass(desc)` resolves/loads it. You must also consider: - **Arrays**: an allowed element type can appear as `[Lcom.example.Foo;` — handle the array form (strip leading `[` / `L`). - **Dynamic proxies**: override `resolveProxyClass(String[] interfaces)` too, since gadget chains often use `java.lang.reflect.Proxy`. - **Allowlist, not blocklist**: list the few classes you actually expect and reject the rest; blocklisting known gadgets misses future ones. ## Worked example ```java class SafeObjectInputStream extends ObjectInputStream { private static final Set<String> ALLOWED = Set.of( "com.example.dto.User", "com.example.dto.Address", "java.util.ArrayList", "java.lang.Integer"); SafeObjectInputStream(InputStream in) throws IOException { super(in); } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); // normalize array element types like [Lcom.example.dto.User; String base = name.replaceAll("^\\[+L?", "").replace(";", ""); if (!ALLOWED.contains(base)) { throw new InvalidClassException("Blocked deserialization", name); } return super.resolveClass(desc); } @Override protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { throw new InvalidClassException("Proxy deserialization not allowed"); } } ``` The check runs once per class as the stream is read — before instantiation — so a forbidden gadget class throws and the chain never fires. ## Relationship to JEP 290 JEP 290's `ObjectInputFilter` performs the **same look-ahead** at the same point in the read, but as a **configurable callback** rather than a subclass — and it adds stream-metric checks (depth, refs, bytes, array length) for DoS bombs. If you're on Java 9+ (or 8u121+), prefer the filter: it's less error-prone (no array/proxy normalization gotchas), composable, and installable per-stream or process-wide. The `resolveClass` technique remains useful for older runtimes or when you can't change how a library constructs its stream but can subclass it. Ready-made: Apache Commons IO's `ValidatingObjectInputStream` implements allow/deny via `accept(...)`/`reject(...)` so you don't hand-roll `resolveClass`. ## Limits Look-ahead gates *which classes* may be built; it cannot vet a permitted class's internal behavior, and a too-broad allowlist re-opens the hole. It is **defense in depth** layered under the primary control: don't natively deserialize untrusted data — use a data-only format into known types. **Summary:** look-ahead = validate the class name before instantiation. Pre-JEP-290, override `resolveClass` (and `resolveProxyClass`) with an allowlist; JEP 290 standardizes the same idea as a configurable `ObjectInputFilter`.
- Why must you also override resolveProxyClass?Many gadget chains use java.lang.reflect.Proxy to invoke handlers during deserialization. If you only filter resolveClass, a proxy-based gadget can slip through, so reject (or strictly validate) proxy interfaces too.
- When would you still use resolveClass instead of JEP 290 filters?On runtimes older than 8u121/Java 9 without the filter API, or when you can subclass a library's stream but cannot set a filter on it. Otherwise prefer ObjectInputFilter — it's configurable and less error-prone.
saying these in an interview costs you the question
- Validating the class after readObject returns (too late — hooks already ran).
- Filtering resolveClass but ignoring arrays and dynamic proxies.
- Using a blocklist of known gadgets instead of an allowlist.
- Treating look-ahead as a full substitute for not deserializing untrusted data.