Skip to content
Permalink
Browse files

Update SecurityJackson2Modules

Fixes gh-4370
  • Loading branch information...
rwinch committed May 31, 2017
1 parent b3a60a8 commit 947d11f433b78294942cb5ea56e8aa5c3a0ca439
@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,17 +16,18 @@

package org.springframework.security.jackson2;

import com.fasterxml.jackson.annotation.JacksonAnnotation;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jsontype.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ClassUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.io.IOException;
import java.util.*;

/**
* This utility class will find all the SecurityModules in classpath.
@@ -65,7 +66,7 @@ public static void enableDefaultTyping(ObjectMapper mapper) {
if(mapper != null) {
TypeResolverBuilder<?> typeBuilder = mapper.getDeserializationConfig().getDefaultTyper(null);
if (typeBuilder == null) {
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
mapper.setDefaultTyping(createWhitelistedDefaultTyping());
}
}
}
@@ -103,4 +104,111 @@ private static Module loadAndGetInstance(String className, ClassLoader loader) {
}
return modules;
}

/**
* Creates a TypeResolverBuilder that performs whitelisting.
* @return a TypeResolverBuilder that performs whitelisting.
*/
private static TypeResolverBuilder<? extends TypeResolverBuilder> createWhitelistedDefaultTyping() {
TypeResolverBuilder<? extends TypeResolverBuilder> result = new WhitelistTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL);
result = result.init(JsonTypeInfo.Id.CLASS, null);
result = result.inclusion(JsonTypeInfo.As.PROPERTY);
return result;
}

/**
* An implementation of {@link ObjectMapper.DefaultTypeResolverBuilder} that overrides the {@link TypeIdResolver}
* with {@link WhitelistTypeIdResolver}.
* @author Rob Winch
*/
static class WhitelistTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {

public WhitelistTypeResolverBuilder(ObjectMapper.DefaultTyping defaultTyping) {
super(defaultTyping);
}

protected TypeIdResolver idResolver(MapperConfig<?> config,
JavaType baseType, Collection<NamedType> subtypes, boolean forSer, boolean forDeser) {
TypeIdResolver result = super.idResolver(config, baseType, subtypes, forSer, forDeser);
return new WhitelistTypeIdResolver(result);
}
}

/**
* A {@link TypeIdResolver} that delegates to an existing implementation and throws an IllegalStateException if the
* class being looked up is not whitelisted, does not provide an explicit mixin, and is not annotated with Jackson
* mappings. See https://github.com/spring-projects/spring-security/issues/4370
*/
static class WhitelistTypeIdResolver implements TypeIdResolver {
private static final Set<String> WHITELIST_CLASS_NAMES = Collections.unmodifiableSet(new HashSet(Arrays.asList(
"java.util.ArrayList",
"java.util.Collections$EmptyMap",
"java.util.Date",
"java.util.TreeMap",
"org.springframework.security.core.context.SecurityContextImpl"
)));

private final TypeIdResolver delegate;

WhitelistTypeIdResolver(TypeIdResolver delegate) {
this.delegate = delegate;
}

@Override
public void init(JavaType baseType) {
delegate.init(baseType);
}

@Override
public String idFromValue(Object value) {
return delegate.idFromValue(value);
}

@Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
return delegate.idFromValueAndType(value, suggestedType);
}

@Override
public String idFromBaseType() {
return delegate.idFromBaseType();
}

@Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
DeserializationConfig config = (DeserializationConfig) context.getConfig();
JavaType result = delegate.typeFromId(context, id);
String className = result.getRawClass().getName();
if(isWhitelisted(className)) {
return delegate.typeFromId(context, id);
}
boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null;
if(isExplicitMixin) {
return result;
}
JacksonAnnotation jacksonAnnotation = AnnotationUtils.findAnnotation(result.getRawClass(), JacksonAnnotation.class);
if(jacksonAnnotation != null) {
return result;
}
throw new IllegalArgumentException("The class with " + id + " and name of " + className + " is not whitelisted. " +
"If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. " +
"If the serialization is only done by a trusted source, you can also enable default typing. " +
"See https://github.com/spring-projects/spring-security/issues/4370 for details");
}

private boolean isWhitelisted(String id) {
return WHITELIST_CLASS_NAMES.contains(id);
}

@Override
public String getDescForKnownTypeIds() {
return delegate.getDescForKnownTypeIds();
}

@Override
public JsonTypeInfo.Id getMechanism() {
return delegate.getMechanism();
}

}
}
@@ -0,0 +1,126 @@
/*
* Copyright 2015-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


package org.springframework.security.jackson2;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonIgnoreType;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;

import java.lang.annotation.*;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

/**
* @author Rob Winch
* @since 5.0
*/
public class SecurityJackson2ModulesTests {
private ObjectMapper mapper;

@Before
public void setup() {
mapper = new ObjectMapper();
SecurityJackson2Modules.enableDefaultTyping(mapper);
}

@Test
public void readValueWhenNotWhitelistedOrMappedThenThrowsException() throws Exception {
String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}";
try {
mapper.readValue(content, Object.class);
fail("Expected Exception");
} catch(RuntimeException e) {
assertThat(e).hasMessageContaining("whitelisted");
}
}

@Test
public void readValueWhenExplicitDefaultTypingAfterSecuritySetupThenReadsAsSpecificType() throws Exception {
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}";

assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelisted.class);
}

@Test
public void readValueWhenExplicitDefaultTypingBeforeSecuritySetupThenReadsAsSpecificType() throws Exception {
mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
SecurityJackson2Modules.enableDefaultTyping(mapper);
String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}";

assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelisted.class);
}

@Test
public void readValueWhenAnnotatedThenReadsAsSpecificType() throws Exception {
String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelistedButAnnotated\",\"property\":\"bar\"}";

assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelistedButAnnotated.class);
}

@Test
public void readValueWhenMixinProvidedThenReadsAsSpecificType() throws Exception {
mapper.addMixIn(NotWhitelisted.class, NotWhitelistedMixin.class);
String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}";

assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelisted.class);
}

@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotJacksonAnnotation {}

@NotJacksonAnnotation
static class NotWhitelisted {
private String property = "bar";

public String getProperty() {
return property;
}

public void setProperty(String property) {
}
}

@JsonIgnoreType(false)
static class NotWhitelistedButAnnotated {
private String property = "bar";

public String getProperty() {
return property;
}

public void setProperty(String property) {
}
}

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class NotWhitelistedMixin {

}
}

0 comments on commit 947d11f

Please sign in to comment.
You can’t perform that action at this time.