Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic protection against untrusted deserialization.
- Loading branch information
Showing
17 changed files
with
964 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
hazelcast-client/src/test/java/com/hazelcast/client/ClientDeserializationProtectionTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
159
hazelcast/src/main/java/com/hazelcast/config/ClassFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 + "}"; | ||
} | ||
|
||
} |
111 changes: 111 additions & 0 deletions
111
hazelcast/src/main/java/com/hazelcast/config/JavaSerializationFilterConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.