Environment
- Dubbo version: 3.2.15 (also affects 3.3.x — the relevant code path is in hessian-lite)
- hessian-lite version: 3.2.13
- JDK: 17
Description
Hessian2SerializerFactory hooks into getDefaultSerializer() to enforce the checkSerializable security check. However, in hessian-lite's SerializerFactory.getSerializer(Class), classes that define a writeReplace() method are handled before getDefaultSerializer() is reached — they get a JavaSerializer directly and skip the getDefaultSerializer override entirely.
This means:
- A class without
Serializable and without writeReplace() → correctly rejected by checkSerializable
- A class without
Serializable but with writeReplace() → bypasses the check, serialization succeeds
The relevant priority chain in SerializerFactory.getSerializer(Class):
1. _staticSerializerMap (built-in types)
2. Custom serializer factories
3. EnumSet check
4. writeReplace check → new JavaSerializer(cl, loader); goto cache ← short-circuits here
5. Map / Collection / Iterator / Enum / ...
6. getDefaultSerializer(cl) ← checkSerializable is here, never reached for step 4
How to reproduce
Define two DTOs:
// No Serializable, no writeReplace
public class NormalDto {
private String name;
// getter/setter
}
// No Serializable, HAS writeReplace
public class WriteReplaceDto {
private String name;
// getter/setter
private Object writeReplace() {
return this;
}
}
Define a Dubbo service that accepts both as parameters:
public interface TestService {
String sendNormal(NormalDto dto);
String sendWriteReplace(WriteReplaceDto dto);
}
Call both methods from a consumer:
sendNormal(new NormalDto("test")) → Fails on consumer side with "Serialized class ... has not implement Serializable interface"
sendWriteReplace(new WriteReplaceDto("test")) → Consumer serializes successfully and sends the message (provider-side deserialization catches it via a different path — loadSerializedClass)
Observed behavior
The security check is inconsistent between serialization and deserialization:
| Side |
Path |
writeReplace class caught? |
| Serialization (sender) |
getSerializer() → writeReplace detected → JavaSerializer directly → bypasses getDefaultSerializer / checkSerializable |
No |
| Deserialization (receiver) |
loadSerializedClass() → DefaultSerializeClassChecker.loadClass() |
Yes |
Expected behavior
The checkSerializable enforcement should be consistent regardless of whether a class has writeReplace(). A class that doesn't implement Serializable should be rejected on both the serialization and deserialization sides.
Suggested fix
Override getSerializer(Class) in Hessian2SerializerFactory to ensure the security check runs before delegating to super.getSerializer(), so that writeReplace classes are also covered. Alternatively, the check could be moved into SerializerFactory.getSerializer(Class) in hessian-lite itself, before the writeReplace branch.
Environment
Description
Hessian2SerializerFactoryhooks intogetDefaultSerializer()to enforce thecheckSerializablesecurity check. However, in hessian-lite'sSerializerFactory.getSerializer(Class), classes that define awriteReplace()method are handled beforegetDefaultSerializer()is reached — they get aJavaSerializerdirectly and skip thegetDefaultSerializeroverride entirely.This means:
Serializableand withoutwriteReplace()→ correctly rejected bycheckSerializableSerializablebut withwriteReplace()→ bypasses the check, serialization succeedsThe relevant priority chain in
SerializerFactory.getSerializer(Class):How to reproduce
Define two DTOs:
Define a Dubbo service that accepts both as parameters:
Call both methods from a consumer:
sendNormal(new NormalDto("test"))→ Fails on consumer side with"Serialized class ... has not implement Serializable interface"sendWriteReplace(new WriteReplaceDto("test"))→ Consumer serializes successfully and sends the message (provider-side deserialization catches it via a different path —loadSerializedClass)Observed behavior
The security check is inconsistent between serialization and deserialization:
getSerializer()→ writeReplace detected →JavaSerializerdirectly → bypassesgetDefaultSerializer/checkSerializableloadSerializedClass()→DefaultSerializeClassChecker.loadClass()Expected behavior
The
checkSerializableenforcement should be consistent regardless of whether a class haswriteReplace(). A class that doesn't implementSerializableshould be rejected on both the serialization and deserialization sides.Suggested fix
Override
getSerializer(Class)inHessian2SerializerFactoryto ensure the security check runs before delegating tosuper.getSerializer(), so that writeReplace classes are also covered. Alternatively, the check could be moved intoSerializerFactory.getSerializer(Class)in hessian-lite itself, before the writeReplace branch.