Skip to content

Commit

Permalink
Add basic protection against untrusted deserialization.
Browse files Browse the repository at this point in the history
  • Loading branch information
kwart committed May 15, 2018
1 parent 7cdb2c6 commit ef4bea0
Show file tree
Hide file tree
Showing 17 changed files with 964 additions and 43 deletions.
Expand Up @@ -46,6 +46,7 @@
import com.hazelcast.nio.SocketInterceptor;
import com.hazelcast.partition.strategy.DefaultPartitioningStrategy;
import com.hazelcast.spi.properties.GroupProperty;
import com.hazelcast.spi.properties.HazelcastProperties;
import com.hazelcast.spi.serialization.SerializationService;
import com.hazelcast.util.function.Supplier;

Expand Down Expand Up @@ -89,6 +90,7 @@ public InternalSerializationService createSerializationService(byte version) {
return builder
.setClassLoader(configClassLoader)
.setConfig(serializationConfig)
.setProperties(new HazelcastProperties(config.getProperties()))
.setManagedContext(new HazelcastClientManagedContext(client, config.getManagedContext()))
.setPartitioningStrategy(partitioningStrategy)
.setHazelcastInstance(hazelcastInstance)
Expand Down
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
*
* 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 com.hazelcast.client;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.io.Serializable;

import org.junit.After;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.client.test.TestAwareClientFactory;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.nio.serialization.HazelcastSerializationException;
import com.hazelcast.spi.properties.GroupProperty;
import com.hazelcast.test.HazelcastParallelClassRunner;
import com.hazelcast.test.annotation.ParallelTest;
import com.hazelcast.test.annotation.QuickTest;

/**
* Tests if deserialization blacklisting works for clients
*/
@RunWith(HazelcastParallelClassRunner.class)
@Category({ QuickTest.class, ParallelTest.class })
public class ClientDeserializationProtectionTest {

private final TestAwareClientFactory factory = new TestAwareClientFactory();

@After
public void killAllHazelcastInstances() throws IOException {
factory.terminateAll();
}

/**
* <pre>
* Given: Serialization filter is configured with a blacklist on the client.
* When: Blacklisted class is deserialized.
* Then: The deserialization fails.
* </pre>
*/
@Test
public void test() throws Exception {
HazelcastInstance hz = factory.newHazelcastInstance(null);
ClientConfig config = new ClientConfig();
config.setProperty(GroupProperty.SERIALIZATION_FILTER_ENABLED.getName(), "true");
config.setProperty(GroupProperty.SERIALIZATION_FILTER_BLACKLIST_CLASSES.getName(), TestDeserialized.class.getName());
HazelcastInstance client = factory.newHazelcastClient(config);

hz.getMap("test").put("test", new TestDeserialized());
try {
client.getMap("test").get("test");
fail("Deserialization should fail");
} catch (HazelcastSerializationException s) {
assertEquals("SecurityException was expected as a cause of failed deserialization", SecurityException.class,
s.getCause().getClass());
assertFalse("Untrusted deserialization was possible", TestDeserialized.IS_DESERIALIZED);
}
}

public static class TestDeserialized implements Serializable {
private static final long serialVersionUID = 1L;
public static volatile boolean IS_DESERIALIZED = false;

private void writeObject(java.io.ObjectOutputStream out) throws IOException {
}

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
IS_DESERIALIZED = true;
}
}
}
159 changes: 159 additions & 0 deletions hazelcast/src/main/java/com/hazelcast/config/ClassFilter.java
@@ -0,0 +1,159 @@
/*
* Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
*
* 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 com.hazelcast.config;

import static com.hazelcast.util.Preconditions.checkNotNull;
import static java.util.Collections.unmodifiableSet;

import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import com.hazelcast.logging.ILogger;
import com.hazelcast.logging.Logger;

/**
* Holds blacklist and whitelist configuration in java deserialization configuration.
*/
public class ClassFilter {

private static final String PROPERTY_CLASSNAME_LIMIT = "hazelcast.serialization.filter.classname.limit";
private static final int CLASSNAME_LIMIT = Integer.getInteger(PROPERTY_CLASSNAME_LIMIT, 10000);
private static final ILogger LOGGER = Logger.getLogger(ClassFilter.class);

private final Set<String> classes = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
private final Set<String> packages = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

private AtomicBoolean warningLogged = new AtomicBoolean();

/**
* Returns unmodifiable set of class names.
*/
public Set<String> getClasses() {
return unmodifiableSet(classes);
}

/**
* Returns unmodifiable set of package names.
*/
public Set<String> getPackages() {
return unmodifiableSet(packages);
}

public ClassFilter addClasses(String... names) {
checkNotNull(names);
for (String name : names) {
classes.add(name);
}
return this;
}

public ClassFilter setClasses(Collection<String> names) {
checkNotNull(names);
classes.clear();
classes.addAll(names);
return this;
}

public ClassFilter addPackages(String... names) {
checkNotNull(names);
for (String name : names) {
packages.add(name);
}
return this;
}

public ClassFilter setPackages(Collection<String> names) {
checkNotNull(names);
packages.clear();
packages.addAll(names);
return this;
}

public boolean isEmpty() {
return classes.isEmpty() && packages.isEmpty();
}

public boolean isListed(String className) {
if (classes.contains(className)) {
return true;
}
if (!packages.isEmpty()) {
int dotPosition = className.lastIndexOf(".");
if (dotPosition > 0) {
// String packageName = ;
return checkPackage(className, className.substring(0, dotPosition));
}
}
return false;
}

/**
* Checks if given class name is listed by package. If it's listed, then performance optimization is used and classname is
* added directly to {@code classes} collection.
*
* @param className Class name to be checked.
* @param packageName Package name of the checked class.
* @return {@code true} iff class is listed by-package
*/
private boolean checkPackage(String className, String packageName) {
if (packages.contains(packageName)) {
if (classes.size() < CLASSNAME_LIMIT) {
// performance optimization
classes.add(className);
} else if (warningLogged.compareAndSet(false, true)) {
LOGGER.warning(String.format(
"The class names collection size reached its limit. Optimizations for package names checks "
+ "will not optimize next usages. You can control the class names collection size limit by "
+ "setting system property '%s'. Actual value is %d.",
PROPERTY_CLASSNAME_LIMIT, CLASSNAME_LIMIT));
}
return true;
}
return false;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((classes == null) ? 0 : classes.hashCode());
result = prime * result + ((packages == null) ? 0 : packages.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ClassFilter other = (ClassFilter) obj;
return ((classes == null && other.classes == null) || (classes != null && classes.equals(other.classes)))
&& ((packages == null && other.packages == null) || (packages != null && packages.equals(other.packages)));
}

@Override
public String toString() {
return "ClassFilter{classes=" + classes + ", packages=" + packages + "}";
}

}
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
*
* 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 com.hazelcast.config;

import static com.hazelcast.spi.properties.GroupProperty.SERIALIZATION_FILTER_BLACKLIST_CLASSES;
import static com.hazelcast.spi.properties.GroupProperty.SERIALIZATION_FILTER_BLACKLIST_PACKAGES;
import static com.hazelcast.spi.properties.GroupProperty.SERIALIZATION_FILTER_ENABLED;
import static com.hazelcast.spi.properties.GroupProperty.SERIALIZATION_FILTER_WHITELIST_CLASSES;
import static com.hazelcast.spi.properties.GroupProperty.SERIALIZATION_FILTER_WHITELIST_PACKAGES;

import com.hazelcast.spi.properties.HazelcastProperties;
import com.hazelcast.util.StringUtil;

/**
* Configuration for Serialization Filter.
*/
public final class JavaSerializationFilterConfig {

private final ClassFilter blacklist;
private final ClassFilter whitelist;

private JavaSerializationFilterConfig(ClassFilter blacklist, ClassFilter whitelist) {
if (blacklist == null) {
blacklist = new ClassFilter();
// default blacklist - some well-known vulnerable classes/packages
blacklist.addClasses(
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"bsh.XThis",
"org.apache.commons.beanutils.BeanComparator",
"org.codehaus.groovy.runtime.ConvertedClosure",
"org.codehaus.groovy.runtime.MethodClosure",
"org.springframework.beans.factory.ObjectFactory",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl")
.addPackages(
"org.apache.commons.collections.functors",
"org.apache.commons.collections4.functors");
}
this.blacklist = blacklist;
this.whitelist = whitelist == null ? new ClassFilter() : whitelist;
}

public static JavaSerializationFilterConfig getInstance(HazelcastProperties hzProperties) {
if (hzProperties.getBoolean(SERIALIZATION_FILTER_ENABLED)) {
return new JavaSerializationFilterConfig(
createClassFilter(hzProperties.getString(SERIALIZATION_FILTER_BLACKLIST_CLASSES),
hzProperties.getString(SERIALIZATION_FILTER_BLACKLIST_PACKAGES)),
createClassFilter(hzProperties.getString(SERIALIZATION_FILTER_WHITELIST_CLASSES),
hzProperties.getString(SERIALIZATION_FILTER_WHITELIST_PACKAGES)));
}
return null;
}

public ClassFilter getBlacklist() {
return blacklist;
}

public ClassFilter getWhitelist() {
return whitelist;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((blacklist == null) ? 0 : blacklist.hashCode());
result = prime * result + ((whitelist == null) ? 0 : whitelist.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
JavaSerializationFilterConfig other = (JavaSerializationFilterConfig) obj;
return ((blacklist == null && other.blacklist == null) || (blacklist != null && blacklist.equals(other.blacklist)))
&& ((whitelist == null && other.whitelist == null) || (whitelist != null && whitelist.equals(other.whitelist)));
}

@Override
public String toString() {
return "JavaSerializationFilterConfig{ blacklist=" + blacklist + ", whitelist=" + whitelist + "}";
}

private static ClassFilter createClassFilter(String csvClasses, String csvPackages) {
if (StringUtil.isNullOrEmpty(csvClasses) && StringUtil.isNullOrEmpty(csvPackages)) {
return null;
}
ClassFilter classFilter = new ClassFilter();
classFilter.addClasses(StringUtil.splitByComma(csvClasses, false))
.addPackages(StringUtil.splitByComma(csvPackages, false));
return classFilter;
}
}
Expand Up @@ -187,6 +187,7 @@ public InternalSerializationService createSerializationService() {

ss = builder.setClassLoader(configClassLoader)
.setConfig(serializationConfig)
.setProperties(node.getProperties())
.setManagedContext(hazelcastInstance.managedContext)
.setPartitioningStrategy(partitioningStrategy)
.setHazelcastInstance(hazelcastInstance)
Expand Down

0 comments on commit ef4bea0

Please sign in to comment.