You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I also inspected the same code on upstream/3.3 (HEAD 3dbba260ca) and upstream/3.4: the relevant lines in PojoUtils.realize1 and DefaultSerializeClassChecker.loadClass are identical, so the gap exists there as well — though I have only run the live reproduction on 3.2.16.
Steps to reproduce this issue
Define a service whose parameter type is a concrete POJO class that does not implement Serializable:
Case A (no class key) — succeeds. Provider's processRiskDto is invoked with a fully populated RiskDto instance, even though RiskDto does not implement Serializable.
Case B (with class key) — fails with the expected:
Caused by: java.lang.IllegalArgumentException: [Serialization Security] Serialized class
com.example.api.dto.RiskDto has not implement Serializable interface.
Current mode is strict check, will disallow to deserialize it by default.
at org.apache.dubbo.common.utils.DefaultSerializeClassChecker.loadClass(DefaultSerializeClassChecker.java:114)
at org.apache.dubbo.common.utils.PojoUtils.realize1(PojoUtils.java:460)
at org.apache.dubbo.common.utils.PojoUtils.realize0(PojoUtils.java:348)
at org.apache.dubbo.common.utils.PojoUtils.realize(PojoUtils.java:250)
at org.apache.dubbo.common.utils.PojoUtils.realize(PojoUtils.java:130)
at org.apache.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:120)
I also verified this on the provider side with Arthas: when Case A is invoked, DefaultSerializeClassChecker.loadClass is never called for RiskDto, while GreetingServiceImpl.processRiskDto is invoked and receives a real RiskDto instance.
What you expected to happen
For a strong-typed call (or for a generic call with a class key), Dubbo enforces the Serializable contract on user POJOs (per DefaultSerializeClassChecker.loadClass and Hessian2SerializerFactory.checkSerializable). The same DTO type behaves inconsistently depending on whether the caller happens to include a class entry in the generic Map:
Call style
Serializable enforced?
Strong-typed RPC
Yes (Hessian2 path)
Generic call, Map withclass key
Yes (loadClass path)
Generic call, Map withoutclass key
No
The third row looks surprising: the same non-Serializable DTO is rejected in two paths and silently accepted in the third. I would expect the Serializable contract to be a property of the type, not of whether the caller chose to include a class field.
The main question I would like the maintainers to clarify is: is this difference intentional, or is it an oversight in the check coverage?
Anything else
Code analysis
The check is wired only into the "load class by name" code path. In PojoUtils.realize1:
// dubbo-common/.../PojoUtils.javaif (pojoinstanceofMap<?, ?> && type != null) {
ObjectclassName = ((Map<Object, Object>) pojo).get("class");
if (classNameinstanceofString) { // ← gateif (!CLASS_NOT_FOUND_CACHE.containsKey(className)) {
try {
type = DefaultSerializeClassChecker.getInstance()
.loadClass(ClassUtils.getClassLoader(), (String) className); // ← only check site
} catch (ClassNotFoundExceptione) {
CLASS_NOT_FOUND_CACHE.put((String) className, NOT_FOUND_VALUE);
}
}
}
// ...// Eventually reaches:dest = newInstance(type); // line 557 / 560 — plain reflection, no check
}
DefaultSerializeClassChecker.loadClass (line 105) is the only place the Serializable check happens on this path. When the Map has no class entry, the if at line 456 short-circuits, the loader is never invoked, and newInstance(type) reaches cls.getDeclaredConstructor().newInstance() directly.
The other Serializable check, Hessian2SerializerFactory.checkSerializable (line 74), is also bypassed in this case because in pure generic invocation the wire only carries HashMap + primitives — Hessian2 never sees the user POJO class on the wire and therefore never registers a serializer for it.
Why this might be unintentional
A few observations that suggest this is a coverage gap rather than a deliberate exemption:
The first version of the check (PR [2.7] Add serializable check for pojo #11430, Feb 2023) placed validateClassinside the if (className instanceof String) block. Subsequent refactors in PR Enhance Check #11419 kept the same gating, suggesting the author was thinking "validate when loading by string name" rather than "validate every type that gets instantiated".
DefaultSerializeClassChecker's class JavaDoc references Fastjson2's ContextAutoTypeBeforeHandler — which is an autotype-string security filter. The Serializable check appears to be a secondary concern bolted onto that filter.
The comment in Hessian2SerializerFactory.checkSerializable describes a "two-checker" model where Hessian2 and DefaultSerializeClassChecker jointly enforce the contract. The generic-no-class-key path slips between both checkers.
There is no unit test that pins the current behavior. In PojoUtilsTest, every test DTO implements Serializable, so the no-class-key + non-Serializable combination is not covered either way.
Question for the maintainers
Could you clarify whether the current behavior is intentional? Either answer is useful:
If intentional (e.g. types resolved from method signatures are considered trusted and exempt from this check), it would be helpful to document that, so users do not unknowingly rely on the inconsistency.
If unintentional, I am happy to follow up with a fix and tests.
Are you willing to submit a pull request to fix on your own?
Yes I am willing to submit a pull request on my own!
Pre-check
Search before asking
Apache Dubbo Component
Java SDK (apache/dubbo)
Dubbo Version
Reproduced on Dubbo Java 3.2.16 (Spring Boot 3.2.4, OpenJDK 17.0.10, macOS 14, hessian2 default serialization).
I also inspected the same code on
upstream/3.3(HEAD3dbba260ca) andupstream/3.4: the relevant lines inPojoUtils.realize1andDefaultSerializeClassChecker.loadClassare identical, so the gap exists there as well — though I have only run the live reproduction on 3.2.16.Steps to reproduce this issue
Define a service whose parameter type is a concrete POJO class that does not implement
Serializable:Provider implementation just echoes the arg.
On the consumer side, invoke it generically through
GenericService. We compare two payloads:Default protocol/serialization (
dubbo+hessian2), defaultdubbo.application.checkSerializable(true).Observed behavior
classkey) — succeeds. Provider'sprocessRiskDtois invoked with a fully populatedRiskDtoinstance, even thoughRiskDtodoes not implementSerializable.classkey) — fails with the expected:I also verified this on the provider side with Arthas: when Case A is invoked,
DefaultSerializeClassChecker.loadClassis never called forRiskDto, whileGreetingServiceImpl.processRiskDtois invoked and receives a realRiskDtoinstance.What you expected to happen
For a strong-typed call (or for a generic call with a
classkey), Dubbo enforces the Serializable contract on user POJOs (perDefaultSerializeClassChecker.loadClassandHessian2SerializerFactory.checkSerializable). The same DTO type behaves inconsistently depending on whether the caller happens to include aclassentry in the generic Map:classkeyloadClasspath)classkeyThe third row looks surprising: the same non-
SerializableDTO is rejected in two paths and silently accepted in the third. I would expect the Serializable contract to be a property of the type, not of whether the caller chose to include aclassfield.The main question I would like the maintainers to clarify is: is this difference intentional, or is it an oversight in the check coverage?
Anything else
Code analysis
The check is wired only into the "load class by name" code path. In
PojoUtils.realize1:DefaultSerializeClassChecker.loadClass(line 105) is the only place the Serializable check happens on this path. When the Map has noclassentry, theifat line 456 short-circuits, the loader is never invoked, andnewInstance(type)reachescls.getDeclaredConstructor().newInstance()directly.The other Serializable check,
Hessian2SerializerFactory.checkSerializable(line 74), is also bypassed in this case because in pure generic invocation the wire only carriesHashMap+ primitives — Hessian2 never sees the user POJO class on the wire and therefore never registers a serializer for it.Why this might be unintentional
A few observations that suggest this is a coverage gap rather than a deliberate exemption:
validateClassinside theif (className instanceof String)block. Subsequent refactors in PR Enhance Check #11419 kept the same gating, suggesting the author was thinking "validate when loading by string name" rather than "validate every type that gets instantiated".DefaultSerializeClassChecker's class JavaDoc references Fastjson2'sContextAutoTypeBeforeHandler— which is an autotype-string security filter. The Serializable check appears to be a secondary concern bolted onto that filter.Hessian2SerializerFactory.checkSerializabledescribes a "two-checker" model where Hessian2 andDefaultSerializeClassCheckerjointly enforce the contract. The generic-no-class-key path slips between both checkers.PojoUtilsTest, every test DTOimplements Serializable, so the no-class-key + non-Serializable combination is not covered either way.Question for the maintainers
Could you clarify whether the current behavior is intentional? Either answer is useful:
Are you willing to submit a pull request to fix on your own?
Code of Conduct