Skip to content

Commit bfbc003

Browse files
committed
Consider DefaultTyping configuration in Jackson Serializers.
We're exploring whether it makes sense to accept a `DefaultTyping` config option for Jackson serializer configuration.
1 parent d8adf86 commit bfbc003

File tree

4 files changed

+170
-29
lines changed

4 files changed

+170
-29
lines changed

src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName,
135135

136136
registerNullValueSerializer(this.mapper, typeHintPropertyName);
137137

138-
this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(getObjectMapper(), typeHintPropertyName));
138+
this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(
139+
GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING, getObjectMapper(), typeHintPropertyName));
139140
}
140141

141142
/**
@@ -216,10 +217,12 @@ private static Lazy<String> getConfiguredTypeDeserializationPropertyName(ObjectM
216217
});
217218
}
218219

219-
private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(ObjectMapper objectMapper,
220+
private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(@Nullable DefaultTyping defaultTyping,
221+
ObjectMapper objectMapper,
220222
@Nullable String typeHintPropertyName) {
221223

222-
StdTypeResolverBuilder typer = TypeResolverBuilder.forEverything(objectMapper).init(JsonTypeInfo.Id.CLASS, null)
224+
StdTypeResolverBuilder typer = TypeResolverBuilder.forTyping(defaultTyping, objectMapper)
225+
.init(JsonTypeInfo.Id.CLASS, null)
223226
.inclusion(As.PROPERTY);
224227

225228
if (StringUtils.hasText(typeHintPropertyName)) {
@@ -464,6 +467,8 @@ public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, Seri
464467
*/
465468
public static class GenericJackson2JsonRedisSerializerBuilder {
466469

470+
private static final DefaultTyping DEFAULT_TYPING = DefaultTyping.EVERYTHING;
471+
467472
private @Nullable String typeHintPropertyName;
468473

469474
private Jackson2ObjectReader reader = Jackson2ObjectReader.create();
@@ -472,7 +477,9 @@ public static class GenericJackson2JsonRedisSerializerBuilder {
472477

473478
private @Nullable ObjectMapper objectMapper;
474479

475-
private @Nullable Boolean defaultTyping;
480+
private @Nullable Boolean defaultTypingEnabled;
481+
482+
private @Nullable DefaultTyping defaultTyping;
476483

477484
private boolean registerNullValueSerializer = true;
478485

@@ -490,6 +497,22 @@ private GenericJackson2JsonRedisSerializerBuilder() {}
490497
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
491498
*/
492499
public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTyping) {
500+
this.defaultTypingEnabled = defaultTyping;
501+
this.defaultTyping = defaultTyping ? DEFAULT_TYPING : null;
502+
return this;
503+
}
504+
505+
/**
506+
* Enable default typing by setting {@link DefaultTyping}. Enabling default typing will override
507+
* {@link ObjectMapper#setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder)} for a given
508+
* {@link ObjectMapper}. Default typing is enabled by default if no {@link ObjectMapper} is provided.
509+
*
510+
* @param defaultTyping the default typing mode.
511+
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
512+
* @since 4.0.3
513+
*/
514+
public GenericJackson2JsonRedisSerializerBuilder defaultTyping(DefaultTyping defaultTyping) {
515+
this.defaultTypingEnabled = true;
493516
this.defaultTyping = defaultTyping;
494517
return this;
495518
}
@@ -599,9 +622,11 @@ public GenericJackson2JsonRedisSerializer build() {
599622
: new NullValueSerializer(this.typeHintPropertyName)));
600623
}
601624

602-
if ((!providedObjectMapper && (defaultTyping == null || defaultTyping))
603-
|| (defaultTyping != null && defaultTyping)) {
604-
objectMapper.setDefaultTyping(createDefaultTypeResolverBuilder(objectMapper, typeHintPropertyName));
625+
// enable default typing by default unless providing ObjectMapper or defaultTypingEnabled is explicitly set.
626+
if ((!providedObjectMapper && (defaultTypingEnabled == null || defaultTypingEnabled))
627+
|| (defaultTypingEnabled != null && defaultTypingEnabled)) {
628+
objectMapper
629+
.setDefaultTyping(createDefaultTypeResolverBuilder(defaultTyping, objectMapper, typeHintPropertyName));
605630
}
606631

607632
return new GenericJackson2JsonRedisSerializer(objectMapper, this.reader, this.writer, this.typeHintPropertyName);
@@ -619,12 +644,17 @@ public GenericJackson2JsonRedisSerializer build() {
619644
*/
620645
private static class TypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
621646

622-
static TypeResolverBuilder forEverything(ObjectMapper mapper) {
623-
return new TypeResolverBuilder(DefaultTyping.EVERYTHING, mapper.getPolymorphicTypeValidator());
647+
private final DefaultTyping typing;
648+
649+
static TypeResolverBuilder forTyping(@Nullable DefaultTyping defaultTyping, ObjectMapper mapper) {
650+
return new TypeResolverBuilder(
651+
defaultTyping == null ? GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING : defaultTyping,
652+
mapper.getPolymorphicTypeValidator());
624653
}
625654

626655
public TypeResolverBuilder(DefaultTyping typing, PolymorphicTypeValidator polymorphicTypeValidator) {
627656
super(typing, polymorphicTypeValidator);
657+
this.typing = typing;
628658
}
629659

630660
@Override
@@ -646,6 +676,10 @@ public boolean useForType(JavaType javaType) {
646676

647677
javaType = resolveArrayOrWrapper(javaType);
648678

679+
if (javaType.isEnumType() && typing != DefaultTyping.EVERYTHING) {
680+
return super.useForType(javaType);
681+
}
682+
649683
if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) {
650684
return false;
651685
}
@@ -655,6 +689,10 @@ public boolean useForType(JavaType javaType) {
655689
return false;
656690
}
657691

692+
if (typing != GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING) {
693+
return super.useForType(javaType);
694+
}
695+
658696
// [databind#88] Should not apply to JSON tree models:
659697
return !TreeNode.class.isAssignableFrom(javaType.getRawClass());
660698
}

src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,12 @@ private static Lazy<String> getConfiguredTypeDeserializationPropertyName(ObjectM
263263
*/
264264
public static class GenericJacksonJsonRedisSerializerBuilder<B extends MapperBuilder<? extends ObjectMapper, ? extends MapperBuilder<?, ?>>> {
265265

266+
private static final DefaultTyping DEFAULT_TYPING = DefaultTyping.NON_FINAL;
267+
266268
private final Supplier<B> builderFactory;
267269

268270
private boolean cacheNullValueSupportEnabled = false;
269-
private boolean defaultTyping = false;
271+
private @Nullable DefaultTyping defaultTyping = null;
270272
private @Nullable String typePropertyName;
271273
private PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
272274
.allowIfBaseType(Object.class).allowIfSubType((ctx, clazz) -> true).build();
@@ -323,7 +325,28 @@ public GenericJacksonJsonRedisSerializerBuilder<B> enableSpringCacheNullValueSup
323325
@Contract("-> this")
324326
public GenericJacksonJsonRedisSerializerBuilder<B> enableUnsafeDefaultTyping() {
325327

326-
this.defaultTyping = true;
328+
withDefaultTyping();
329+
return this;
330+
}
331+
332+
/**
333+
* Enables
334+
* {@link JsonMapper.Builder#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
335+
* default typing} without any type validation constraints.
336+
* <p>
337+
* <strong>WARNING</strong>: without restrictions of the {@link PolymorphicTypeValidator} deserialization is
338+
* vulnerable to arbitrary code execution when reading from untrusted sources.
339+
*
340+
* @param defaultTyping the default typing mode to use.
341+
* @return {@code this} builder.
342+
* @since 4.0.3
343+
* @see <a href=
344+
* "https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data">https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data</a>
345+
*/
346+
@Contract("_ -> this")
347+
public GenericJacksonJsonRedisSerializerBuilder<B> defaultTyping(DefaultTyping defaultTyping) {
348+
349+
this.defaultTyping = defaultTyping;
327350
return this;
328351
}
329352

@@ -338,8 +361,8 @@ public GenericJacksonJsonRedisSerializerBuilder<B> enableUnsafeDefaultTyping() {
338361
public GenericJacksonJsonRedisSerializerBuilder<B> enableDefaultTyping(PolymorphicTypeValidator typeValidator) {
339362

340363
typeValidator(typeValidator);
364+
withDefaultTyping();
341365

342-
this.defaultTyping = true;
343366
return this;
344367
}
345368

@@ -372,6 +395,15 @@ public GenericJacksonJsonRedisSerializerBuilder<B> typePropertyName(String typeP
372395
return this;
373396
}
374397

398+
/**
399+
* Enable default typing using {@link DefaultTyping#NON_FINAL} if not already configured.
400+
*/
401+
private void withDefaultTyping() {
402+
if (this.defaultTyping == null) {
403+
defaultTyping(DEFAULT_TYPING);
404+
}
405+
}
406+
375407
/**
376408
* Configures the {@link JacksonObjectWriter}.
377409
*
@@ -440,10 +472,10 @@ public GenericJacksonJsonRedisSerializer build() {
440472
}));
441473
}
442474

443-
if (defaultTyping) {
475+
if (defaultTyping != null) {
444476

445477
GenericJacksonJsonRedisSerializer.TypeResolverBuilder resolver = new GenericJacksonJsonRedisSerializer.TypeResolverBuilder(
446-
typeValidator, DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.Id.CLASS, typePropertyName);
478+
typeValidator, defaultTyping, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.Id.CLASS, typePropertyName);
447479

448480
mapperBuilder.configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false)
449481
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).setDefaultTyping(resolver);
@@ -596,17 +628,13 @@ public void setupModule(SetupContext context) {
596628

597629
private static class TypeResolverBuilder extends DefaultTypeResolverBuilder {
598630

599-
public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping t, JsonTypeInfo.As includeAs) {
600-
super(subtypeValidator, t, includeAs);
601-
}
602-
603-
public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping t, String propertyName) {
604-
super(subtypeValidator, t, propertyName);
605-
}
631+
private final DefaultTyping defaultTyping;
606632

607-
public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping t, JsonTypeInfo.As includeAs,
633+
public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping defaultTyping,
634+
JsonTypeInfo.As includeAs,
608635
JsonTypeInfo.Id idType, @Nullable String propertyName) {
609-
super(subtypeValidator, t, includeAs, idType, propertyName);
636+
super(subtypeValidator, defaultTyping, includeAs, idType, propertyName);
637+
this.defaultTyping = defaultTyping;
610638
}
611639

612640
@Override
@@ -628,6 +656,10 @@ public boolean useForType(JavaType javaType) {
628656

629657
javaType = resolveArrayOrWrapper(javaType);
630658

659+
if (javaType.isEnumType() && defaultTyping != GenericJacksonJsonRedisSerializerBuilder.DEFAULT_TYPING) {
660+
return super.useForType(javaType);
661+
}
662+
631663
if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) {
632664
return false;
633665
}
@@ -637,6 +669,10 @@ public boolean useForType(JavaType javaType) {
637669
return false;
638670
}
639671

672+
if (defaultTyping != GenericJacksonJsonRedisSerializerBuilder.DEFAULT_TYPING) {
673+
return super.useForType(javaType);
674+
}
675+
640676
// [databind#88] Should not apply to JSON tree models:
641677
return !TreeNode.class.isAssignableFrom(javaType.getRawClass());
642678
}

src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
* @author Mark Paluch
6565
* @author John Blum
6666
*/
67+
@SuppressWarnings("removal")
6768
class GenericJackson2JsonRedisSerializerUnitTests {
6869

6970
private static final SimpleObject SIMPLE_OBJECT = new SimpleObject(1L);
@@ -402,6 +403,40 @@ void deserializesEnumFromBytes() {
402403
.isEqualTo(EnumType.TWO);
403404
}
404405

406+
@Test // GH-3306
407+
void serializesNonFinalIntoBytesWithTypeHint() {
408+
409+
GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
410+
.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build();
411+
412+
assertThat(new String(serializer.serialize(EnumType.ONE)))
413+
.isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName()));
414+
}
415+
416+
@Test // GH-3306
417+
void deserializesEnumFromBytesWithTypeHint() {
418+
419+
GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
420+
.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build();
421+
422+
assertThat(serializer.deserialize(
423+
"[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class))
424+
.isEqualTo(EnumType.TWO);
425+
}
426+
427+
@Test // GH-3306
428+
void serializesRecordIntoBytesWithout() {
429+
430+
GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
431+
.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build();
432+
433+
record Foo(String hello) {
434+
435+
}
436+
437+
assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}");
438+
}
439+
405440
@Test // GH-2396
406441
void serializesJavaTimeIntoBytes() {
407442

src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,21 +344,53 @@ void deserializesUUIDFromBytes() {
344344

345345
@Test // GH-2396
346346
void serializesEnumIntoBytes() {
347-
348-
GenericJacksonJsonRedisSerializer serializer = this.serializer;
349-
350-
assertThat(serializer.serialize(EnumType.ONE)).isEqualTo(("\"ONE\"").getBytes(StandardCharsets.UTF_8));
347+
assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\""));
351348
}
352349

353350
@Test // GH-2396
354351
void deserializesEnumFromBytes() {
355352

356-
GenericJacksonJsonRedisSerializer serializer = this.serializer;
357-
358353
assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class))
359354
.isEqualTo(EnumType.TWO);
360355
}
361356

357+
@Test // GH-3306
358+
void serializesEnumIntoBytesWithTypeHint() {
359+
360+
GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> {
361+
it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS);
362+
});
363+
364+
assertThat(new String(serializer.serialize(EnumType.ONE)))
365+
.isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName()));
366+
}
367+
368+
@Test // GH-3306
369+
void deserializesEnumFromBytesWithTypeHint() {
370+
371+
GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> {
372+
it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS);
373+
});
374+
375+
assertThat(serializer.deserialize(
376+
"[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class))
377+
.isEqualTo(EnumType.TWO);
378+
}
379+
380+
@Test // GH-3306
381+
void serializesRecordIntoBytesWithoutHint() {
382+
383+
GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> {
384+
it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS);
385+
});
386+
387+
record Foo(String hello) {
388+
389+
}
390+
391+
assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}");
392+
}
393+
362394
@Test // GH-2396
363395
void serializesJavaTimeIntoBytes() {
364396

0 commit comments

Comments
 (0)