Validate generic type arguments - #6254
martinfrancois wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Looks good overall. The new tests fail on 2.18 without this change (4 of 15) and pass with it. The rest of the suite passes too; the only failure, a timezone-name assertion in DateSerializationTest, also fails on the base commit.
Two behaviour changes are probably worth a line in the release notes:
- Stricter:
Map<String,Integer>read with type idjava.util.HashMap<java.lang.String,java.lang.Long>now fails withInvalidTypeIdException. Before, it was accepted and putLongvalues into the map. That is the correct behaviour, but someone may be relying on the old one. - Looser: with
allowIfBaseType(Animal.class),List<Animal>read with type idjava.util.ArrayList<Dog>is now accepted; before, the PTV rejected it. This follows from callingvalidateBaseTypeon the declared parameter type. It seems fine, sinceDogis checked to be anAnimal, but it is a change.
(Object type arguments are still accepted as before, e.g. HashMap<Object,Object> into Map<String,Integer>.)
Inline comments below are minor.
| // Object is the canonical placeholder for wildcards, unbound parameters, | ||
| // and runtime-erased values; it cannot itself instantiate an attacker- | ||
| // selected class either. | ||
| if (param.isPrimitive() || param.isJavaLangObject()) { |
There was a problem hiding this comment.
The method javadoc above (L376-395) is out of date now: enums still get the new subtype check and the SubTypeValidator check, so they're only exempt from the PTV. Primitives are now skipped, and the base used is the declared type parameter, not the outer base type.
There was a problem hiding this comment.
Rewrote the javadoc. It now says that declaredType is the type parameter the base type declares at the same position (or the unknown type if there is none), that primitives and Object skip all checks, and that enums skip only the PolymorphicTypeValidator while still being checked against the declared type and the built-in deny list. I also renamed the parameter from baseType to declaredType in the helpers so the code reads the way the javadoc describes it.
| } | ||
| for (int i = 0, n = param.containedTypeCount(); i < n; ++i) { | ||
| _validateTypeParameter(baseType, param.containedType(i), ptv, config); | ||
| if (this instanceof DeserializationContext) { |
There was a problem hiding this comment.
Minor: an instanceof DeserializationContext check in the base class is a bit of a layering smell. A protected no-op hook in DatabindContext, overridden in DeserializationContext, would be cleaner.
There was a problem hiding this comment.
Agreed. DatabindContext now has a protected no-op _validateGenericSubType(JavaType) hook, and DeserializationContext overrides it with the SubTypeValidator call. The instanceof check is gone.
| } | ||
| if (!param.isTypeOrSubTypeOf(baseType.getRawClass())) { | ||
| throw invalidTypeIdException(baseType, rawName, | ||
| "Type parameter is not a subtype of its declared generic base"); |
There was a problem hiding this comment.
Error message: baseType here is the declared type parameter, so users see e.g. "Could not resolve type id 'java.lang.Long' as a subtype of java.lang.Integer". It may not be obvious that this relates to their polymorphic Map property. Could the message also include the outer polymorphic base type?
There was a problem hiding this comment.
Changed. The exception now carries the outer polymorphic base type and the complete type id, the same way the container-level checks do, and the extra description names the offending argument. For your example the message is now:
Could not resolve type id 'java.util.HashMap<java.lang.String,java.lang.Long>' as a subtype of `java.util.Map<java.lang.String,java.lang.Integer>`: type parameter `java.lang.Long` (declared as `java.lang.Integer`) is not a subtype of its declared type
PTV denials follow the same shape (... denied resolution of type parameter com.example.Evil(declared asjava.lang.Object)). getTypeId() returns the full id and getBaseType() the polymorphic base. Covered by incompatibleTypeArgumentMessageNamesPolymorphicBaseAndTypeId and the extended allowedOuterBaseDoesNotApproveGenericArguments.
| // tree (and array component, if any) and validate each node. | ||
| // The container itself was already validated above. Validate its type | ||
| // parameters against the corresponding declared base parameters. | ||
| _validateTypeParameters(baseType, subType, ptv, config); |
There was a problem hiding this comment.
The old top-level subType.isArrayType() branch is gone. As far as I can tell, a canonical type id containing < can't parse to a top-level array type, so nothing is lost. Can you confirm that's intended?
There was a problem hiding this comment.
Intended, and I checked it against TypeParser rather than assuming. The parser only produces an ArrayType when the class-name token itself is an array class ([Lfoo;, [I), and TypeBindings.create rejects any type argument list for such a class because arrays declare no type variables:
[Ljava.lang.String;<java.lang.Integer> -> IllegalArgumentException: Cannot create TypeBindings for class [Ljava.lang.String; with 1 type parameter: class expects 0
[I<java.lang.Integer> -> same
[Ljava.util.List;<> -> IllegalArgumentException: Cannot locate class '>'
java.util.List<java.lang.String>[] -> IllegalArgumentException: Unexpected tokens after complete type
So a type id that reaches _resolveAndValidateGeneric (it contains <) can never resolve to a top-level array type, and the branch had no reachable input. Array arguments (ArrayList<Evil[]>) are still unwrapped in _validateTypeParameter.
| final void _validateGenericSubType(JavaType type) throws JsonMappingException | ||
| { | ||
| SubTypeValidator.instance().validateSubType(this, type, | ||
| getConfig().introspect(type)); |
There was a problem hiding this comment.
introspect(type) builds a bean description for every type argument, but SubTypeValidator.validateSubType only uses it when it reports a failure. Only generic type ids reach this path, so the cost is small, but the work is wasted in the normal case.
There was a problem hiding this comment.
Switched to introspectClassAnnotations(type). SubTypeValidator.validateSubType only needs getBeanClass()/getType() from the description to build the error, so resolving class-level annotations is enough; no property collection happens any more on the success path.
| @Override | ||
| public Validity validateSubClassName(MapperConfig<?> config, | ||
| JavaType baseType, String subClassName) throws JsonMappingException { | ||
| if (baseType == _allowedBaseType) { |
There was a problem hiding this comment.
== works for the StdTypeResolverBuilder → ClassNameIdResolver path, which passes the same instance. A custom TypeResolverBuilder that builds an equal but different JavaType would get a spurious rejection here. _allowedBaseType.equals(baseType) would be more robust at almost no cost (same at L43).
There was a problem hiding this comment.
Changed both checks to _allowedBaseType.equals(baseType). Added BaseTypeAllowingValidatorTest, which builds an equal but distinct JavaType through a second TypeFactory cache and checks that it is still approved while a different base type still goes to the delegate.
|
@cowtowncoder this change seems too much of a change for a patch - even gitar says it is a High Risk. |
|
Thanks for the careful read. All six inline points are addressed in the new commit; details in the threads. On the two behaviour changes: I added a One more fix came out of re-reviewing the projection logic: when a subtype's projection onto the base contains a self-reference ( Verified locally: the polymorphic validation classes ( |
d0bc119 to
455cd79
Compare
…nceof, validate self-referenced arguments - Error messages now carry the polymorphic base type and the complete type id, the same as the container-level checks, and name the offending type argument and the type parameter it was declared as. - Replace the `instanceof DeserializationContext` check with a protected no-op hook in `DatabindContext` that `DeserializationContext` overrides. - Use `introspectClassAnnotations` for the `SubTypeValidator` call; the bean description is only used to report a denial. - Compare the approved base type with `equals` in `BaseTypeAllowingValidator`. - Validate the type a `ResolvedRecursiveType` refers to: the projection of `SelfSupplier<X>` onto `Supplier` yields `Supplier<SelfSupplier<X>>` whose argument is a self-reference with no type parameters of its own, so `X` was never validated. - Bring the `_validateTypeParameter` javadoc up to date and add a release note naming both behaviour changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
455cd79 to
74f7f1b
Compare
CI failed: Fuzzer build failure due to a package namespace mismatch between the fuzzer script and jackson-databind.Overview1 log analysis completed, revealing 1 build failure related to OSS-Fuzz compilation where the fuzzer fails to locate the expected jackson-databind package. FailuresFuzzer Compilation Error (confidence: high)
Summary
Code Review ✅ Approved🔴 High risk · Changes security-sensitive validation of polymorphic generic type arguments. Adds validation of generic type arguments to prevent invalid type configurations. No issues found. Tip Comment OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
|
@martinfrancois Could you create alternative PR for simplified version -- bit easier to read diffs that way. |
Sure I can @cowtowncoder! Would you prefer it to target the branch from this PR (to better see the differences between this and the alternative) or also against 2.18? |
|
@cowtowncoder I opened #6259 against 2.18. If you'd rather have it against the branch of this PR instead, let me know and I'll change it. |
Summary by Gitar
DatabindContextBaseTypeAllowingValidatorand integration tests for generic type compatibilityThis will update automatically on new commits.