Skip to content

Classes with writeReplace() bypass Hessian2 Serializable security check #16287

Description

@uuuyuqi

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:

  1. A class without Serializable and without writeReplace() → correctly rejected by checkSerializable
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions