This is an automated email from the ASF dual-hosted git repository.
tkobayas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-drools.git
The following commit(s) were added to refs/heads/main by this push:
new 11792686e81 improve protobuf session validation with deserialization
(#6823)
11792686e81 is described below
commit 11792686e8123949c5db590426806f6564cfec7e
Author: Toshiya Kobayashi <[email protected]>
AuthorDate: Thu Jul 23 22:26:42 2026 +0900
improve protobuf session validation with deserialization (#6823)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
.../base/common/DroolsObjectInputStream.java | 2 +-
.../SerializablePlaceholderResolverStrategy.java | 4 +
.../core/util/DeserializationFilterHelper.java | 150 ++++++
.../org/drools/core/util/KeyStoreConstants.java | 5 +
.../marshaller/JPAPlaceholderResolverStrategy.java | 17 +-
.../DeserializationFilterTestSupport.java | 40 ++
.../session/JpaPersistentStatefulSessionTest.java | 17 +-
.../map/impl/JpaBasedPersistenceTest.java | 5 +-
.../map/impl/MapBasedPersistenceTest.java | 11 +-
.../session/JpaPersistentStatefulSessionTest.java | 3 +
.../integrationtests/TimerAndCalendarTest.java | 7 +-
drools-serialization-protobuf/pom.xml | 7 +-
.../serialization/protobuf/PersisterHelper.java | 40 +-
.../JavaSerializableResolverStrategy.java | 6 +-
.../protobuf/DeserializationFilterTestSupport.java | 40 ++
.../serialization/protobuf/MarshallerTest.java | 14 +
.../serialization/protobuf/MarshallingTest.java | 28 +
.../protobuf/MarshallingValidationCheckTest.java | 566 +++++++++++++++++++++
.../drools/serialization/protobuf/QueryTest.java | 11 +
.../protobuf/SerializationHelper.java | 21 +
.../protobuf/TruthMaintenanceTest.java | 16 +
.../serialization/protobuf/UnmarshallingTest.java | 12 +
.../testdata/enhancement/DeserializationProbe.java | 33 ++
.../test/java/testdata/enhancement/EnumProbe.java | 29 ++
.../java/testdata/enhancement/StaticInitProbe.java | 34 ++
.../src/test/resources/logback-test.xml | 31 ++
.../integrationtests/BackwardChainingTest.java | 21 +
.../compiler/integrationtests/CepEspTest.java | 15 +
.../IncrementalCompilationTest.java | 13 +
.../regression/EventDeserializationInPastTest.java | 11 +
.../LogicalInsertionsSerializationTest.java | 12 +
.../factmodel/traits/LogicalTraitTest.java | 86 ++++
.../traits/TraitMarshallingValidationTest.java | 133 +++++
33 files changed, 1412 insertions(+), 28 deletions(-)
diff --git
a/drools-base/src/main/java/org/drools/base/common/DroolsObjectInputStream.java
b/drools-base/src/main/java/org/drools/base/common/DroolsObjectInputStream.java
index 9eb8ba090a1..0a4149bbf75 100644
---
a/drools-base/src/main/java/org/drools/base/common/DroolsObjectInputStream.java
+++
b/drools-base/src/main/java/org/drools/base/common/DroolsObjectInputStream.java
@@ -82,7 +82,7 @@ public class DroolsObjectInputStream extends ObjectInputStream
}
protected Class resolveClass(String className) throws
ClassNotFoundException {
- return ClassUtils.getClassFromName( className, true, this.classLoader
);
+ return ClassUtils.getClassFromName( className, false, this.classLoader
);
}
protected Class< ? > resolveClass(ObjectStreamClass desc) throws
IOException,
diff --git
a/drools-core/src/main/java/org/drools/core/marshalling/SerializablePlaceholderResolverStrategy.java
b/drools-core/src/main/java/org/drools/core/marshalling/SerializablePlaceholderResolverStrategy.java
index 13ae30b6d46..2c4642bf3a5 100644
---
a/drools-core/src/main/java/org/drools/core/marshalling/SerializablePlaceholderResolverStrategy.java
+++
b/drools-core/src/main/java/org/drools/core/marshalling/SerializablePlaceholderResolverStrategy.java
@@ -24,6 +24,7 @@ import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
+import org.drools.core.util.DeserializationFilterHelper;
import org.kie.api.marshalling.ObjectMarshallingStrategy;
import org.kie.api.marshalling.ObjectMarshallingStrategyAcceptor;
@@ -92,6 +93,9 @@ public class SerializablePlaceholderResolverStrategy
@SuppressWarnings("unchecked")
public void read(ObjectInputStream ois) throws IOException,
ClassNotFoundException {
+ if (DeserializationFilterHelper.isDeserializationFilterEnabled()) {
+
ois.setObjectInputFilter(DeserializationFilterHelper.createDeserializationFilter());
+ }
this.data = (List<Object>) ois.readObject();
}
diff --git
a/drools-core/src/main/java/org/drools/core/util/DeserializationFilterHelper.java
b/drools-core/src/main/java/org/drools/core/util/DeserializationFilterHelper.java
new file mode 100644
index 00000000000..29ce1da123a
--- /dev/null
+++
b/drools-core/src/main/java/org/drools/core/util/DeserializationFilterHelper.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.drools.core.util;
+
+import java.io.ObjectInputFilter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DeserializationFilterHelper {
+
+ private static final Logger logger =
LoggerFactory.getLogger(DeserializationFilterHelper.class);
+
+ private static final List<String> DEFAULT_ALLOWED_PREFIXES = List.of(
+ "java.lang.", "java.util.", "java.math.", "java.time.");
+
+ private static final Set<String> DEFAULT_ALLOWED_CLASSES = Set.of(
+ "org.drools.base.reteoo.InitialFactImpl",
+ "org.drools.core.util.LinkedListEntry",
+ "org.drools.tms.SimpleMode",
+ "org.drools.tms.DefeasibleMode",
+ "org.drools.traits.core.factmodel.AbstractTriple",
+ "org.drools.traits.core.factmodel.BitMaskKey",
+ "org.drools.traits.core.factmodel.ExternalizableLinkedHashMap",
+ "org.drools.traits.core.factmodel.Key",
+ "org.drools.traits.core.factmodel.NullTraitType",
+ "org.drools.traits.core.factmodel.ThingProxyImplPlaceHolder",
+ "org.drools.traits.core.factmodel.TraitFieldDefaultValue",
+ "org.drools.traits.core.factmodel.TraitFieldImpl",
+
"org.drools.traits.core.factmodel.TraitFieldImpl$DefaultValueHierarchy",
+ "org.drools.traits.core.factmodel.TraitFieldImpl$TypeComparator",
+ "org.drools.traits.core.factmodel.TraitFieldTMSImpl",
+ "org.drools.traits.core.factmodel.TraitProxyImpl",
+
"org.drools.traits.core.factmodel.TraitRegistryImpl$CachingHierarcyEncoderImpl",
+ "org.drools.traits.core.factmodel.TraitTypeMapImpl",
+ "org.drools.traits.core.factmodel.TripleBasedBean",
+ "org.drools.traits.core.factmodel.TripleBasedStruct",
+ "org.drools.traits.core.factmodel.TripleFactoryImpl",
+ "org.drools.traits.core.factmodel.TripleImpl",
+ "org.drools.traits.core.factmodel.TripleStore",
+ "org.drools.traits.core.factmodel.TypeCache",
+ "org.drools.traits.core.factmodel.TypeHierarchy",
+ "org.drools.traits.core.factmodel.TypeWrapper",
+ "org.drools.traits.core.util.AbstractBitwiseHierarchyImpl",
+
"org.drools.traits.core.util.AbstractBitwiseHierarchyImpl$HierCodeComparator",
+ "org.drools.traits.core.util.AbstractCodedHierarchyImpl",
+ "org.drools.traits.core.util.CodedHierarchyImpl",
+ "org.drools.traits.core.util.HierarchyEncoderImpl",
+ "org.drools.traits.core.util.HierarchyEncoderImpl$ImmutableBitSet",
+ "org.drools.traits.core.util.HierNode",
+ "org.drools.util.bitmask.EmptyButLastBitMask",
+ "org.drools.util.bitmask.LongBitMask",
+ "org.drools.util.bitmask.SingleLongBitMask");
+
+ private DeserializationFilterHelper() {
+ }
+
+ public static boolean isClassAllowed(String className) {
+ if (DEFAULT_ALLOWED_CLASSES.contains(className)) {
+ return true;
+ }
+ for (String prefix : DEFAULT_ALLOWED_PREFIXES) {
+ if (className.startsWith(prefix)) {
+ return true;
+ }
+ }
+ for (String prefix : getUserAllowedPrefixes()) {
+ if (className.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isDeserializationFilterEnabled() {
+ return Boolean.parseBoolean(
+ System.getProperty(KeyStoreConstants.PROP_ENABLE_DESER_FILTER,
"true"));
+ }
+
+ public static ObjectInputFilter createDeserializationFilter() {
+ return filterInfo -> {
+ Class<?> clazz = filterInfo.serialClass();
+ if (clazz == null) {
+ return ObjectInputFilter.Status.UNDECIDED;
+ }
+ if (clazz.isPrimitive()) {
+ return ObjectInputFilter.Status.ALLOWED;
+ }
+ if (clazz.isArray()) {
+ Class<?> componentType = clazz.getComponentType();
+ while (componentType.isArray()) {
+ componentType = componentType.getComponentType();
+ }
+ if (componentType.isPrimitive()) {
+ return ObjectInputFilter.Status.ALLOWED;
+ }
+ clazz = componentType;
+ }
+ String className = clazz.getName();
+ if (isClassAllowed(className)) {
+ return ObjectInputFilter.Status.ALLOWED;
+ }
+ logger.warn("Deserialization of class '{}' was rejected by the
ObjectInputFilter. "
+ + "If this is a legitimate fact class, add it to the
allowlist via "
+ + "-D{}=<pattern>",
+ className,
KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ return ObjectInputFilter.Status.REJECTED;
+ };
+ }
+
+ private static List<String> getUserAllowedPrefixes() {
+ String patterns =
System.getProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS, "");
+ if (patterns.isEmpty()) {
+ return List.of();
+ }
+ List<String> prefixes = new ArrayList<>();
+ for (String pattern : patterns.split(";")) {
+ String trimmed = pattern.trim();
+ if (!trimmed.isEmpty()) {
+ if (trimmed.endsWith(".*")) {
+ prefixes.add(trimmed.substring(0, trimmed.length() - 1));
+ } else if (trimmed.endsWith("*")) {
+ prefixes.add(trimmed.substring(0, trimmed.length() - 1));
+ } else {
+ prefixes.add(trimmed);
+ }
+ }
+ }
+ return prefixes;
+ }
+}
diff --git
a/drools-core/src/main/java/org/drools/core/util/KeyStoreConstants.java
b/drools-core/src/main/java/org/drools/core/util/KeyStoreConstants.java
index 2786b1bb8db..de02a3aaa31 100644
--- a/drools-core/src/main/java/org/drools/core/util/KeyStoreConstants.java
+++ b/drools-core/src/main/java/org/drools/core/util/KeyStoreConstants.java
@@ -46,4 +46,9 @@ public class KeyStoreConstants {
// true if you allow verifying with old sign algorithm "MD5withRSA"
public static final String PROP_VERIFY_OLD_SIGN =
"drools.serialization.verify.old.sign";
+
+ // true (default) to enable ObjectInputFilter on deserialization
+ public static final String PROP_ENABLE_DESER_FILTER =
"drools.serialization.enableDeserializationFilter";
+ // semicolon-separated package patterns to allow during deserialization
(e.g. "com.mycompany.*;org.example.*")
+ public static final String PROP_ALLOWED_DESER_CLASS_PATTERNS =
"drools.serialization.allowedDeserializationClassPatterns";
}
diff --git
a/drools-persistence/drools-persistence-jpa/src/main/java/org/drools/persistence/jpa/marshaller/JPAPlaceholderResolverStrategy.java
b/drools-persistence/drools-persistence-jpa/src/main/java/org/drools/persistence/jpa/marshaller/JPAPlaceholderResolverStrategy.java
index 0d0bc492c02..7b07aaa5402 100644
---
a/drools-persistence/drools-persistence-jpa/src/main/java/org/drools/persistence/jpa/marshaller/JPAPlaceholderResolverStrategy.java
+++
b/drools-persistence/drools-persistence-jpa/src/main/java/org/drools/persistence/jpa/marshaller/JPAPlaceholderResolverStrategy.java
@@ -33,6 +33,7 @@ import jakarta.persistence.Persistence;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.Metamodel;
import org.drools.base.common.DroolsObjectInputStream;
+import org.drools.core.util.DeserializationFilterHelper;
import org.drools.persistence.api.TransactionAware;
import org.drools.persistence.api.TransactionManager;
import org.drools.serialization.protobuf.ProtobufProcessMarshallerWriteContext;
@@ -122,11 +123,7 @@ public class JPAPlaceholderResolverStrategy implements
ObjectMarshallingStrategy
}
public Object read(ObjectInputStream is) throws IOException,
ClassNotFoundException {
- String canonicalName = is.readUTF();
- Object id = is.readObject();
-
- EntityManager em = getEntityManager();
- return em.find(Class.forName(canonicalName), id);
+ return readEntity(is, null);
}
public byte[] marshal(Context context,
@@ -172,11 +169,19 @@ public class JPAPlaceholderResolverStrategy implements
ObjectMarshallingStrategy
}
DroolsObjectInputStream is = new DroolsObjectInputStream( new
ByteArrayInputStream( object ), clToUse );
+ return readEntity(is, clToUse);
+ }
+
+ private Object readEntity(ObjectInputStream is, ClassLoader classloader)
throws IOException, ClassNotFoundException {
+ if (DeserializationFilterHelper.isDeserializationFilterEnabled()) {
+
is.setObjectInputFilter(DeserializationFilterHelper.createDeserializationFilter());
+ }
String canonicalName = is.readUTF();
Object id = is.readObject();
EntityManager em = getEntityManager();
- return em.find(Class.forName(canonicalName, true,
(clToUse==null?this.getClass().getClassLoader():clToUse)), id);
+ ClassLoader cl = classloader != null ? classloader :
this.getClass().getClassLoader();
+ return em.find(Class.forName(canonicalName, true, cl), id);
}
public Context createContext() {
diff --git
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/DeserializationFilterTestSupport.java
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/DeserializationFilterTestSupport.java
new file mode 100644
index 00000000000..b88aed3451c
--- /dev/null
+++
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/DeserializationFilterTestSupport.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.drools.persistence;
+
+import org.drools.core.util.KeyStoreConstants;
+
+public class DeserializationFilterTestSupport {
+
+ private String savedAllowedPatterns;
+
+ public void setUp(String... patterns) {
+ savedAllowedPatterns =
System.getProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ System.setProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+ String.join(";", patterns));
+ }
+
+ public void tearDown() {
+ if (savedAllowedPatterns != null) {
+
System.setProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
savedAllowedPatterns);
+ } else {
+
System.clearProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+ }
+}
diff --git
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/kie/persistence/session/JpaPersistentStatefulSessionTest.java
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/kie/persistence/session/JpaPersistentStatefulSessionTest.java
index 554cac69f03..de4bd0cc0a2 100644
---
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/kie/persistence/session/JpaPersistentStatefulSessionTest.java
+++
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/kie/persistence/session/JpaPersistentStatefulSessionTest.java
@@ -74,25 +74,28 @@ public class JpaPersistentStatefulSessionTest {
private static Logger logger =
LoggerFactory.getLogger(JpaPersistentStatefulSessionTest.class);
private Map<String, Object> context;
private Environment env;
-
+ private final org.drools.persistence.DeserializationFilterTestSupport
filterSupport = new org.drools.persistence.DeserializationFilterTestSupport();
+
public static Stream<String> parameters() {
return Stream.of(OPTIMISTIC_LOCKING, PESSIMISTIC_LOCKING);
- };
-
+ }
+
@BeforeEach
public void setUp() throws Exception {
+ filterSupport.setUp("org.drools.mvel.compiler.Person");
context =
DroolsPersistenceUtil.setupWithPoolingDataSource(DROOLS_PERSISTENCE_UNIT_NAME);
env = createEnvironment(context);
}
-
+
private void setUpLocking(String locking) {
- if(PESSIMISTIC_LOCKING.equals(locking)) {
+ if(PESSIMISTIC_LOCKING.equals(locking)) {
env.set(EnvironmentName.USE_PESSIMISTIC_LOCKING, true);
}
}
-
+
@AfterEach
public void tearDown() throws Exception {
+ filterSupport.tearDown();
DroolsPersistenceUtil.cleanUp(context);
}
@@ -360,6 +363,8 @@ public class JpaPersistentStatefulSessionTest {
public void testSharedReferences(String locking) {
setUpLocking(locking);
KieServices ks = KieServices.Factory.get();
+ KieFileSystem kfs = ks.newKieFileSystem();
+ ks.newKieBuilder(kfs).buildAll();
KieBase kbase =
ks.newKieContainer(ks.getRepository().getDefaultReleaseId()).getKieBase();
KieSession ksession = ks.getStoreServices().newKieSession( kbase,
null, env );
diff --git
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/JpaBasedPersistenceTest.java
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/JpaBasedPersistenceTest.java
index da9879391f3..579679b36c4 100644
---
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/JpaBasedPersistenceTest.java
+++
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/JpaBasedPersistenceTest.java
@@ -46,11 +46,12 @@ import static
org.kie.api.runtime.EnvironmentName.USE_PESSIMISTIC_LOCKING;
public class JpaBasedPersistenceTest extends MapPersistenceTest {
private static Logger logger =
LoggerFactory.getLogger(JPAPlaceholderResolverStrategy.class);
-
+
private Map<String, Object> context;
private EntityManagerFactory emf;
private JtaTransactionManager txm;
private boolean useTransactions = false;
+ private final org.drools.persistence.DeserializationFilterTestSupport
filterSupport = new org.drools.persistence.DeserializationFilterTestSupport();
public static Stream<String> parameters() {
return Stream.of(OPTIMISTIC_LOCKING, PESSIMISTIC_LOCKING);
@@ -59,6 +60,7 @@ public class JpaBasedPersistenceTest extends
MapPersistenceTest {
@BeforeEach
public void setUp() throws Exception {
+ filterSupport.setUp("org.drools.persistence.map.impl.Buddy");
context =
DroolsPersistenceUtil.setupWithPoolingDataSource(DROOLS_PERSISTENCE_UNIT_NAME);
emf = (EntityManagerFactory) context.get(ENTITY_MANAGER_FACTORY);
@@ -74,6 +76,7 @@ public class JpaBasedPersistenceTest extends
MapPersistenceTest {
@AfterEach
public void tearDown() throws Exception {
+ filterSupport.tearDown();
DroolsPersistenceUtil.cleanUp(context);
}
diff --git
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/MapBasedPersistenceTest.java
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/MapBasedPersistenceTest.java
index 107a63bee29..a0dd2d871f4 100644
---
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/MapBasedPersistenceTest.java
+++
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/map/impl/MapBasedPersistenceTest.java
@@ -39,17 +39,24 @@ import org.kie.api.runtime.EnvironmentName;
import org.kie.api.runtime.KieSession;
public class MapBasedPersistenceTest extends MapPersistenceTest{
-
+
private SimpleKnowledgeSessionStorage storage;
-
+ private final org.drools.persistence.DeserializationFilterTestSupport
filterSupport = new org.drools.persistence.DeserializationFilterTestSupport();
+
public static Stream<String> parameters() {
return Stream.of("not relevant");
};
@BeforeEach
public void createStorage(){
+ filterSupport.setUp("org.drools.persistence.map.impl.Buddy");
storage = new SimpleKnowledgeSessionStorage();
}
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+ filterSupport.tearDown();
+ }
@Override
protected KieSession createSession(String locking, KieBase kbase) {
diff --git
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/session/JpaPersistentStatefulSessionTest.java
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/session/JpaPersistentStatefulSessionTest.java
index f092a2fed72..e1afb525be2 100644
---
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/session/JpaPersistentStatefulSessionTest.java
+++
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/session/JpaPersistentStatefulSessionTest.java
@@ -72,6 +72,7 @@ public class JpaPersistentStatefulSessionTest {
private Map<String, Object> context;
private Environment env;
+ private final org.drools.persistence.DeserializationFilterTestSupport
filterSupport = new org.drools.persistence.DeserializationFilterTestSupport();
public static Stream<String> parameters() {
return Stream.of(OPTIMISTIC_LOCKING, PESSIMISTIC_LOCKING);
@@ -79,6 +80,7 @@ public class JpaPersistentStatefulSessionTest {
@BeforeEach
public void setUp() throws Exception {
+ filterSupport.setUp("org.drools.mvel.compiler.Person");
context =
DroolsPersistenceUtil.setupWithPoolingDataSource(DROOLS_PERSISTENCE_UNIT_NAME);
env = createEnvironment(context);
}
@@ -92,6 +94,7 @@ public class JpaPersistentStatefulSessionTest {
@AfterEach
public void tearDown() throws Exception {
+ filterSupport.tearDown();
DroolsPersistenceUtil.cleanUp(context);
}
diff --git
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/timer/integrationtests/TimerAndCalendarTest.java
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/timer/integrationtests/TimerAndCalendarTest.java
index 2223b958fa8..f675a463d9a 100644
---
a/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/timer/integrationtests/TimerAndCalendarTest.java
+++
b/drools-persistence/drools-persistence-jpa/src/test/java/org/drools/persistence/timer/integrationtests/TimerAndCalendarTest.java
@@ -67,20 +67,23 @@ import static
org.drools.persistence.util.DroolsPersistenceUtil.createEnvironmen
import static
org.drools.persistence.util.DroolsPersistenceUtil.setupWithPoolingDataSource;
public class TimerAndCalendarTest {
-
+
private Map<String, Object> context;
+ private final org.drools.persistence.DeserializationFilterTestSupport
filterSupport = new org.drools.persistence.DeserializationFilterTestSupport();
public static Stream<String> parameters() {
return Stream.of(OPTIMISTIC_LOCKING, PESSIMISTIC_LOCKING);
};
-
+
@BeforeEach
public void before() throws Exception {
+ filterSupport.setUp("org.drools.test.TestEvent");
context = setupWithPoolingDataSource(DROOLS_PERSISTENCE_UNIT_NAME);
}
@AfterEach
public void after() throws Exception {
+ filterSupport.tearDown();
cleanUp(context);
}
diff --git a/drools-serialization-protobuf/pom.xml
b/drools-serialization-protobuf/pom.xml
index 79bdbb62216..da83676407b 100644
--- a/drools-serialization-protobuf/pom.xml
+++ b/drools-serialization-protobuf/pom.xml
@@ -104,6 +104,11 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<profiles>
@@ -148,4 +153,4 @@
</profile>
</profiles>
-</project>
\ No newline at end of file
+</project>
diff --git
a/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/PersisterHelper.java
b/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/PersisterHelper.java
index 976004373b0..689987cdf3a 100644
---
a/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/PersisterHelper.java
+++
b/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/PersisterHelper.java
@@ -38,6 +38,8 @@ import
org.drools.tms.beliefsystem.simple.BeliefSystemLogicalCallback;
import org.drools.base.common.DroolsObjectInputStream;
import org.drools.base.common.DroolsObjectOutputStream;
import org.drools.core.common.WorkingMemoryAction;
+import org.drools.core.util.DeserializationFilterHelper;
+import org.drools.core.util.KeyStoreConstants;
import org.drools.base.factmodel.traits.TraitFactory;
import org.drools.core.impl.InternalRuleBase;
import org.drools.core.impl.WorkingMemoryReteExpireAction;
@@ -169,7 +171,8 @@ public class PersisterHelper extends MarshallingHelper {
}
byte[] buff = payload.toByteArray();
- sign( _header, buff );
+ byte[] headerFieldsBytes = _header.build().toByteArray();
+ sign( _header, buildSignedData( buff, headerFieldsBytes ) );
_header.setPayload( ByteString.copyFrom( buff ) );
context.write( _header.build().toByteArray() );
@@ -233,13 +236,15 @@ public class PersisterHelper extends MarshallingHelper {
}
private static ProtobufMessages.Header loadStrategiesCheckSignature(
MarshallerReaderContext context, ProtobufMessages.Header _header) throws
ClassNotFoundException, IOException {
- loadStrategiesIndex( context, _header );
-
byte[] sessionbuff = _header.getPayload().toByteArray();
+ ProtobufMessages.Header headerFields = _header.toBuilder()
+ .clearSignature()
+ .clearPayload()
+ .build();
+ checkSignature( _header, buildSignedData( sessionbuff,
headerFields.toByteArray() ) );
+
+ loadStrategiesIndex( context, _header );
- // should we check version as well here?
- checkSignature( _header, sessionbuff );
-
return _header;
}
@@ -284,7 +289,7 @@ public class PersisterHelper extends MarshallingHelper {
classLoader =
context.getKnowledgeBase().getRootClassLoader();
}
if ( classLoader instanceof ProjectClassLoader ) {
- readRuntimeDefinedClasses( _header, (ProjectClassLoader)
classLoader );
+ readRuntimeDefinedClasses( _header, (ProjectClassLoader)
classLoader, context.getKnowledgeBase() );
}
ctx.read( new DroolsObjectInputStream(
_entry.getData().newInput(), classLoader) );
}
@@ -292,10 +297,22 @@ public class PersisterHelper extends MarshallingHelper {
}
public static void readRuntimeDefinedClasses( Header _header,
- ProjectClassLoader pcl )
throws IOException, ClassNotFoundException {
+ ProjectClassLoader pcl,
+ InternalRuleBase kBase )
throws IOException, ClassNotFoundException {
if ( _header.getRuntimeClassDefinitionsCount() > 0 ) {
+ TraitFactory traitFactory = kBase != null ?
RuntimeComponentFactory.get().getTraitFactory(kBase) : null;
+ if ( traitFactory == null ) {
+ throw new RuntimeException( "RuntimeClassDef entries found but
no TraitFactory is available. Deserialization aborted." );
+ }
for ( ProtobufMessages.RuntimeClassDef def :
_header.getRuntimeClassDefinitionsList() ) {
String resourceName = def.getClassFqName();
+ String className = resourceName.replace('/',
'.').replace(".class", "");
+ if ( !DeserializationFilterHelper.isClassAllowed( className )
) {
+ throw new RuntimeException( "RuntimeClassDef '" +
resourceName
+ + "' is not in the deserialization allowlist. "
+ + "If this is a legitimate trait class, add it via
-D"
+ +
KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS + "=<pattern>" );
+ }
byte[] byteCode = def.getClassDef().toByteArray();
if ( ! pcl.getStore().containsKey( resourceName ) ) {
pcl.getStore().put(resourceName, byteCode);
@@ -340,6 +357,13 @@ public class PersisterHelper extends MarshallingHelper {
}
}
+ private static byte[] buildSignedData(byte[] payloadBytes, byte[]
headerFieldsBytes) {
+ byte[] result = new byte[payloadBytes.length +
headerFieldsBytes.length];
+ System.arraycopy(payloadBytes, 0, result, 0, payloadBytes.length);
+ System.arraycopy(headerFieldsBytes, 0, result, payloadBytes.length,
headerFieldsBytes.length);
+ return result;
+ }
+
public static ExtensionRegistry buildRegistry( MarshallerReaderContext
context, ProcessMarshaller processMarshaller ) {
ExtensionRegistry registry = ExtensionRegistry.newInstance();
if( processMarshaller != null ) {
diff --git
a/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/marshalling/JavaSerializableResolverStrategy.java
b/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/marshalling/JavaSerializableResolverStrategy.java
index 5bb6badc1fa..cd43bc268b9 100644
---
a/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/marshalling/JavaSerializableResolverStrategy.java
+++
b/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/marshalling/JavaSerializableResolverStrategy.java
@@ -25,6 +25,7 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
+import org.drools.core.util.DeserializationFilterHelper;
import org.kie.api.marshalling.ObjectMarshallingStrategy;
import org.kie.api.marshalling.ObjectMarshallingStrategyAcceptor;
@@ -72,9 +73,12 @@ public class JavaSerializableResolverStrategy
is = new ObjectInputStream(bs) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws
ClassNotFoundException {
- return Class.forName(desc.getName(), true, classloader);
+ return Class.forName(desc.getName(), false, classloader);
}
};
+ if (DeserializationFilterHelper.isDeserializationFilterEnabled()) {
+
is.setObjectInputFilter(DeserializationFilterHelper.createDeserializationFilter());
+ }
return read(is);
} catch (Exception e) {
throw new RuntimeException(e);
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/DeserializationFilterTestSupport.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/DeserializationFilterTestSupport.java
new file mode 100644
index 00000000000..63eefa3cf0f
--- /dev/null
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/DeserializationFilterTestSupport.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.drools.serialization.protobuf;
+
+import org.drools.core.util.KeyStoreConstants;
+
+public class DeserializationFilterTestSupport {
+
+ private String savedAllowedPatterns;
+
+ public void setUp(String... patterns) {
+ savedAllowedPatterns =
System.getProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ System.setProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+ String.join(";", patterns));
+ }
+
+ public void tearDown() {
+ if (savedAllowedPatterns != null) {
+
System.setProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
savedAllowedPatterns);
+ } else {
+
System.clearProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+ }
+}
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallerTest.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallerTest.java
index b83a8637c2d..31873bac1a6 100644
---
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallerTest.java
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallerTest.java
@@ -47,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class MarshallerTest {
private Environment env = EnvironmentFactory.newEnvironment();
+ private final DeserializationFilterTestSupport filterSupport = new
DeserializationFilterTestSupport();
public static Stream<ObjectMarshallingStrategy> parameters() {
return Stream.of(new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT),
@@ -57,6 +58,19 @@ public class MarshallerTest {
this.env.set( EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, new
ObjectMarshallingStrategy[]{ strategy } );
}
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+ filterSupport.setUp("org.drools.mvel.compiler.Person",
+ "org.drools.serialization.protobuf.MarshallerTest$LongFact",
+ "org.drools.serialization.protobuf.MarshallerTest$LongFacts");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+ filterSupport.tearDown();
+ }
+
+
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
public void testAgendaDoNotSerializeObject(ObjectMarshallingStrategy
strategy) throws Exception {
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallingTest.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallingTest.java
index b66f1e5bdb9..8b5b3dfd90f 100644
---
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallingTest.java
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallingTest.java
@@ -111,6 +111,34 @@ import static
org.drools.serialization.protobuf.SerializationHelper.getSerialise
public class MarshallingTest extends CommonTestMethodBase {
+ private final DeserializationFilterTestSupport filterSupport = new
DeserializationFilterTestSupport();
+
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+ filterSupport.setUp("org.drools.mvel.compiler.Address",
+ "org.drools.mvel.compiler.Alarm",
+ "org.drools.mvel.compiler.Cell",
+ "org.drools.mvel.compiler.Cheese",
+ "org.drools.mvel.compiler.CheeseEqual",
+ "org.drools.mvel.compiler.FactA",
+ "org.drools.mvel.compiler.FactB",
+ "org.drools.mvel.compiler.FactC",
+ "org.drools.mvel.compiler.Message",
+ "org.drools.mvel.compiler.Person",
+ "org.drools.mvel.compiler.Primitives",
+ "org.drools.mvel.compiler.Sensor",
+ "org.drools.serialization.protobuf.MarshallingTest$A",
+ "org.drools.serialization.protobuf.MarshallingTest$B",
+ "org.drools.serialization.protobuf.MarshallingTest$C",
+ "defaultpkg.Employee",
+ "defaultpkg.Person");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+ filterSupport.tearDown();
+ }
+
@Test
public void testSerializable() throws Exception {
Collection<KiePackage> kpkgs =
loadKnowledgePackages("test_Serializable.drl" );
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallingValidationCheckTest.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallingValidationCheckTest.java
new file mode 100644
index 00000000000..98e7943f8b3
--- /dev/null
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/MarshallingValidationCheckTest.java
@@ -0,0 +1,566 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.drools.serialization.protobuf;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.URL;
+
+import com.google.protobuf.ByteString;
+import org.drools.core.impl.EnvironmentFactory;
+import org.drools.core.marshalling.ClassObjectMarshallingStrategyAcceptor;
+import org.drools.core.marshalling.SerializablePlaceholderResolverStrategy;
+import org.drools.core.util.KeyStoreConstants;
+import org.drools.core.util.KeyStoreHelper;
+import org.drools.kiesession.rulebase.InternalKnowledgeBase;
+import
org.drools.serialization.protobuf.marshalling.JavaSerializableResolverStrategy;
+import org.drools.wiring.api.classloader.ProjectClassLoader;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.io.ResourceType;
+import org.kie.api.marshalling.ObjectMarshallingStrategy;
+import org.kie.api.runtime.Environment;
+import org.kie.api.runtime.EnvironmentName;
+import org.kie.api.runtime.KieSession;
+import org.kie.internal.marshalling.MarshallerFactory;
+import org.kie.internal.utils.KieHelper;
+import testdata.enhancement.DeserializationProbe;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class MarshallingValidationCheckTest {
+
+ private final DeserializationFilterTestSupport filterSupport = new
DeserializationFilterTestSupport();
+
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {
+ filterSupport.setUp();
+ }
+
+ @AfterEach
+ void tearDown() {
+ filterSupport.tearDown();
+ System.clearProperty(KeyStoreConstants.PROP_ENABLE_DESER_FILTER);
+ System.clearProperty(KeyStoreConstants.PROP_SIGN);
+ System.clearProperty(KeyStoreConstants.PROP_PVT_KS_URL);
+ System.clearProperty(KeyStoreConstants.PROP_PVT_KS_PWD);
+ System.clearProperty(KeyStoreConstants.PROP_PVT_ALIAS);
+ System.clearProperty(KeyStoreConstants.PROP_PVT_PWD);
+ System.clearProperty(KeyStoreConstants.PROP_PUB_KS_URL);
+ System.clearProperty(KeyStoreConstants.PROP_PUB_KS_PWD);
+ KeyStoreHelper.reInit();
+ DeserializationProbe.deserialized = false;
+ }
+
+ @Test
+ void testDeserializationFilterRejectsUnexpectedClasses() throws Exception {
+ byte[] probeBytes = serializeObject(new DeserializationProbe());
+
+ JavaSerializableResolverStrategy strategy =
+ new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT);
+
+ assertThatThrownBy(() ->
+ strategy.unmarshal(null, null, probeBytes,
getClass().getClassLoader()))
+ .as("Deserialization of non-whitelisted class should be
rejected by the ObjectInputFilter")
+ .isInstanceOf(RuntimeException.class);
+
+ assertThat(DeserializationProbe.deserialized)
+ .as("DeserializationProbe.readObject() should NOT have been
invoked")
+ .isFalse();
+ }
+
+ @Test
+ void testDeserializationFilterRejectsEnum() throws Exception {
+ byte[] enumBytes =
serializeObject(testdata.enhancement.EnumProbe.INSTANCE);
+
+ JavaSerializableResolverStrategy strategy =
+ new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT);
+
+ testdata.enhancement.EnumProbe.initialized = false;
+
+ assertThatThrownBy(() ->
+ strategy.unmarshal(null, null, enumBytes,
getClass().getClassLoader()))
+ .as("Deserialization of non-whitelisted enum should be
rejected — "
+ + "enums can execute code in static initializers and
constructors")
+ .isInstanceOf(RuntimeException.class);
+
+ assertThat(testdata.enhancement.EnumProbe.initialized)
+ .as("EnumProbe static initializer should NOT have been
invoked")
+ .isFalse();
+ }
+
+ @Test
+ void testTamperedFactHandleDeserialization() throws Exception {
+ String drl =
+ "rule R1 when\n" +
+ " String()\n" +
+ "then\n" +
+ "end\n";
+
+ ObjectMarshallingStrategy[] strategies = new
ObjectMarshallingStrategy[]{
+ new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT)
+ };
+
+ Environment env = EnvironmentFactory.newEnvironment();
+ env.set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, strategies);
+
+ KieBase kbase = new KieHelper().addContent(drl,
ResourceType.DRL).build();
+ KieSession ksession = kbase.newKieSession(null, env);
+ ksession.insert("test-fact");
+ ksession.fireAllRules();
+
+ ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase, strategies);
+
+ byte[] serializedSession;
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ marshaller.marshall(bos, ksession,
ksession.getSessionClock().getCurrentTime());
+ serializedSession = bos.toByteArray();
+ }
+ ksession.dispose();
+
+ byte[] probeBytes = serializeObject(new DeserializationProbe());
+
+ byte[] protobufBytes = extractProtobufBytes(serializedSession);
+ ProtobufMessages.Header header =
ProtobufMessages.Header.parseFrom(protobufBytes);
+ ProtobufMessages.KnowledgeSession session =
+
ProtobufMessages.KnowledgeSession.parseFrom(header.getPayload());
+
+ ProtobufMessages.KnowledgeSession.Builder sessionBuilder =
session.toBuilder();
+ boolean replaced = false;
+ for (int epIdx = 0; epIdx <
session.getRuleData().getEntryPointCount(); epIdx++) {
+ ProtobufMessages.EntryPoint ep =
session.getRuleData().getEntryPoint(epIdx);
+ for (int fhIdx = 0; fhIdx < ep.getHandleCount(); fhIdx++) {
+ ProtobufMessages.FactHandle fh = ep.getHandle(fhIdx);
+ if (fh.hasObject()) {
+ ProtobufMessages.FactHandle tamperedFh = fh.toBuilder()
+ .setObject(ByteString.copyFrom(probeBytes))
+ .build();
+ ProtobufMessages.EntryPoint tamperedEp = ep.toBuilder()
+ .setHandle(fhIdx, tamperedFh)
+ .build();
+
sessionBuilder.setRuleData(session.getRuleData().toBuilder()
+ .setEntryPoint(epIdx, tamperedEp)
+ .build());
+ replaced = true;
+ break;
+ }
+ }
+ if (replaced) {
+ break;
+ }
+ }
+
+ assertThat(replaced)
+ .as("Should have found a fact handle to tamper with")
+ .isTrue();
+
+ ProtobufMessages.Header tamperedHeader = header.toBuilder()
+
.setPayload(ByteString.copyFrom(sessionBuilder.build().toByteArray()))
+ .build();
+ byte[] tamperedSession =
wrapInObjectStream(tamperedHeader.toByteArray());
+
+ assertThatThrownBy(() ->
+ marshaller.unmarshall(new
ByteArrayInputStream(tamperedSession)))
+ .as("Unmarshalling a tampered payload with a non-whitelisted
class should fail")
+ .isInstanceOf(RuntimeException.class);
+
+ assertThat(DeserializationProbe.deserialized)
+ .as("DeserializationProbe.readObject() should NOT have been
invoked during unmarshalling "
+ + "of a tampered payload — the deserialization filter
should block it")
+ .isFalse();
+ }
+
+ @Test
+ void testDeserializationAllowsJdkTypes() throws Exception {
+ String drl =
+ "rule R1 when\n" +
+ " String()\n" +
+ "then\n" +
+ "end\n";
+
+ ObjectMarshallingStrategy[] strategies = new
ObjectMarshallingStrategy[]{
+ new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT)
+ };
+
+ Environment env = EnvironmentFactory.newEnvironment();
+ env.set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, strategies);
+
+ KieBase kbase = new KieHelper().addContent(drl,
ResourceType.DRL).build();
+ KieSession ksession = kbase.newKieSession(null, env);
+ ksession.insert("hello");
+ ksession.fireAllRules();
+
+ ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase, strategies);
+
+ byte[] serializedSession;
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ marshaller.marshall(bos, ksession,
ksession.getSessionClock().getCurrentTime());
+ serializedSession = bos.toByteArray();
+ }
+ ksession.dispose();
+
+ KieSession restored = marshaller.unmarshall(new
ByteArrayInputStream(serializedSession));
+ assertThat(restored.getObjects().iterator().next()).isEqualTo("hello");
+ restored.dispose();
+ }
+
+ @Test
+ void testDeserializationAllowsConfiguredPatterns() throws Exception {
+ System.setProperty(KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+ "testdata.enhancement.*");
+
+ byte[] probeBytes = serializeObject(new DeserializationProbe());
+
+ JavaSerializableResolverStrategy strategy =
+ new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT);
+
+ Object result = strategy.unmarshal(null, null, probeBytes,
getClass().getClassLoader());
+ assertThat(result).isInstanceOf(DeserializationProbe.class);
+ assertThat(DeserializationProbe.deserialized).isTrue();
+ }
+
+ @Test
+ void testDeserializationFilterOptOut() throws Exception {
+ System.setProperty(KeyStoreConstants.PROP_ENABLE_DESER_FILTER,
"false");
+
+ byte[] probeBytes = serializeObject(new DeserializationProbe());
+
+ JavaSerializableResolverStrategy strategy =
+ new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT);
+
+ Object result = strategy.unmarshal(null, null, probeBytes,
getClass().getClassLoader());
+ assertThat(result).isInstanceOf(DeserializationProbe.class);
+ assertThat(DeserializationProbe.deserialized).isTrue();
+ }
+
+ @Test
+ void testSignatureMustCoverRuntimeClassDefinitions() throws Exception {
+ setPrivateKeyProperties();
+ setPublicKeyProperties();
+
+ String drl =
+ "rule R1 when\n" +
+ " String()\n" +
+ "then\n" +
+ "end\n";
+
+ KieBase kbase = new KieHelper().addContent(drl,
ResourceType.DRL).build();
+
+ ObjectMarshallingStrategy[] strategies = new
ObjectMarshallingStrategy[]{
+ new
SerializablePlaceholderResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT)
+ };
+ Environment env = EnvironmentFactory.newEnvironment();
+ env.set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, strategies);
+
+ KieSession ksession = kbase.newKieSession(null, env);
+ ksession.insert("test");
+ ksession.fireAllRules();
+
+ ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase, strategies);
+
+ byte[] serializedSession;
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ marshaller.marshall(bos, ksession,
ksession.getSessionClock().getCurrentTime());
+ serializedSession = bos.toByteArray();
+ }
+ ksession.dispose();
+
+ byte[] protobufBytes = extractProtobufBytes(serializedSession);
+ ProtobufMessages.Header originalHeader =
ProtobufMessages.Header.parseFrom(protobufBytes);
+
+ byte[] fakeBytecode = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte)
0xBA, (byte) 0xBE};
+
+ ProtobufMessages.Header tamperedHeader = originalHeader.toBuilder()
+
.addRuntimeClassDefinitions(ProtobufMessages.RuntimeClassDef.newBuilder()
+ .setClassFqName("com/example/Payload")
+ .setClassDef(ByteString.copyFrom(fakeBytecode))
+ .build())
+ .build();
+
+ byte[] tamperedSession =
wrapInObjectStream(tamperedHeader.toByteArray());
+
+ assertThatThrownBy(() ->
+ marshaller.unmarshall(new
ByteArrayInputStream(tamperedSession)))
+ .as("Signature check should detect that RuntimeClassDef
entries were "
+ + "tampered — currently the signature only covers the
payload, "
+ + "not the full Header")
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("Signature");
+ }
+
+ @Test
+ void testSignatureCheckedBeforeBytecodeLoading() throws Exception {
+ setPrivateKeyProperties();
+ setPublicKeyProperties();
+
+ String drl =
+ "rule R1 when\n" +
+ " String()\n" +
+ "then\n" +
+ "end\n";
+
+ KieBase kbase = new KieHelper().addContent(drl,
ResourceType.DRL).build();
+
+ ObjectMarshallingStrategy[] strategies = new
ObjectMarshallingStrategy[]{
+ new
SerializablePlaceholderResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT)
+ };
+ Environment env = EnvironmentFactory.newEnvironment();
+ env.set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, strategies);
+
+ KieSession ksession = kbase.newKieSession(null, env);
+ ksession.insert("test");
+ ksession.fireAllRules();
+
+ ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase, strategies);
+
+ byte[] serializedSession;
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ marshaller.marshall(bos, ksession,
ksession.getSessionClock().getCurrentTime());
+ serializedSession = bos.toByteArray();
+ }
+ ksession.dispose();
+
+ ProjectClassLoader pcl = (ProjectClassLoader) ((InternalKnowledgeBase)
kbase).getRootClassLoader();
+ pcl.storeClass("dummy.Placeholder", "dummy/Placeholder.class", new
byte[0]);
+
+ byte[] protobufBytes = extractProtobufBytes(serializedSession);
+ ProtobufMessages.Header originalHeader =
ProtobufMessages.Header.parseFrom(protobufBytes);
+
+ String injectedClassName = "com/example/Payload";
+ byte[] fakeBytecode = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte)
0xBA, (byte) 0xBE};
+
+ ProtobufMessages.Header tamperedHeader = originalHeader.toBuilder()
+
.addRuntimeClassDefinitions(ProtobufMessages.RuntimeClassDef.newBuilder()
+ .setClassFqName(injectedClassName)
+ .setClassDef(ByteString.copyFrom(fakeBytecode))
+ .build())
+ .build();
+
+ byte[] tamperedSession =
wrapInObjectStream(tamperedHeader.toByteArray());
+
+ assertThatThrownBy(() ->
+ marshaller.unmarshall(new
ByteArrayInputStream(tamperedSession)))
+ .as("Signature check should reject the tampered header")
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("Signature");
+
+ assertThat(pcl.getStore().containsKey(injectedClassName))
+ .as("Bytecode for injected class '%s' must NOT be in the
classloader "
+ + "— checkSignature() must run before
loadStrategiesIndex() so "
+ + "that a rejected payload never loads bytecode",
+ injectedClassName)
+ .isFalse();
+ }
+
+ @Test
+ void testUnsignedSessionRejectsInjectedRuntimeClassDef() throws Exception {
+ String drl =
+ "rule R1 when\n" +
+ " String()\n" +
+ "then\n" +
+ "end\n";
+
+ KieBase kbase = new KieHelper().addContent(drl,
ResourceType.DRL).build();
+
+ ObjectMarshallingStrategy[] strategies = new
ObjectMarshallingStrategy[]{
+ new
SerializablePlaceholderResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT)
+ };
+ Environment env = EnvironmentFactory.newEnvironment();
+ env.set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, strategies);
+
+ KieSession ksession = kbase.newKieSession(null, env);
+ ksession.insert("test");
+ ksession.fireAllRules();
+
+ ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase, strategies);
+
+ byte[] serializedSession;
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ marshaller.marshall(bos, ksession,
ksession.getSessionClock().getCurrentTime());
+ serializedSession = bos.toByteArray();
+ }
+ ksession.dispose();
+
+ ProjectClassLoader pcl = (ProjectClassLoader) ((InternalKnowledgeBase)
kbase).getRootClassLoader();
+ pcl.storeClass("dummy.Placeholder", "dummy/Placeholder.class", new
byte[0]);
+
+ byte[] protobufBytes = extractProtobufBytes(serializedSession);
+ ProtobufMessages.Header originalHeader =
ProtobufMessages.Header.parseFrom(protobufBytes);
+
+ String injectedClassName = "example/Payload";
+ byte[] fakeBytecode = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte)
0xBA, (byte) 0xBE};
+
+ ProtobufMessages.Header tamperedHeader = originalHeader.toBuilder()
+
.addRuntimeClassDefinitions(ProtobufMessages.RuntimeClassDef.newBuilder()
+ .setClassFqName(injectedClassName)
+ .setClassDef(ByteString.copyFrom(fakeBytecode))
+ .build())
+ .build();
+
+ byte[] tamperedSession =
wrapInObjectStream(tamperedHeader.toByteArray());
+
+ assertThatThrownBy(() ->
+ marshaller.unmarshall(new
ByteArrayInputStream(tamperedSession)))
+ .as("Unmarshalling with injected RuntimeClassDef should fail "
+ + "— without drools-traits on the classpath,
TraitFactory is null "
+ + "and readRuntimeDefinedClasses() rejects all
RuntimeClassDef entries")
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("no TraitFactory is available");
+
+ assertThat(pcl.getStore().containsKey(injectedClassName))
+ .as("Bytecode for injected class '%s' must NOT be in the
classloader store",
+ injectedClassName)
+ .isFalse();
+ }
+
+ @Test
+ void testReadRuntimeDefinedClassesRejectsNonTraitClasses() throws
Exception {
+ String injectedClassName = "org/drools/example/Payload";
+ byte[] fakeBytecode = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte)
0xBA, (byte) 0xBE};
+
+ ProtobufMessages.Header header = ProtobufMessages.Header.newBuilder()
+
.addRuntimeClassDefinitions(ProtobufMessages.RuntimeClassDef.newBuilder()
+ .setClassFqName(injectedClassName)
+ .setClassDef(ByteString.copyFrom(fakeBytecode))
+ .build())
+ .build();
+
+ java.util.Map<String, byte[]> store = new java.util.HashMap<>();
+ ProjectClassLoader pcl = ProjectClassLoader.createProjectClassLoader(
+ getClass().getClassLoader(), store);
+
+ assertThatThrownBy(() ->
+ PersisterHelper.readRuntimeDefinedClasses(header, pcl, null))
+ .as("readRuntimeDefinedClasses() should reject when no
TraitFactory is available")
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("no TraitFactory is available");
+
+ assertThat(store.containsKey(injectedClassName))
+ .as("Rejected class must not be in the classloader store")
+ .isFalse();
+ }
+
+ private void setPrivateKeyProperties() {
+ URL serverKeyStoreURL =
getClass().getResource("droolsServer.keystore");
+ System.setProperty(KeyStoreConstants.PROP_SIGN, "true");
+ System.setProperty(KeyStoreConstants.PROP_PVT_KS_URL,
serverKeyStoreURL.toExternalForm());
+ System.setProperty(KeyStoreConstants.PROP_PVT_KS_PWD, "serverpwd");
+ System.setProperty(KeyStoreConstants.PROP_PVT_ALIAS, "droolsKey");
+ System.setProperty(KeyStoreConstants.PROP_PVT_PWD, "keypwd");
+ KeyStoreHelper.reInit();
+ }
+
+ private void setPublicKeyProperties() {
+ URL clientKeyStoreURL =
getClass().getResource("droolsClient.keystore");
+ System.setProperty(KeyStoreConstants.PROP_SIGN, "true");
+ System.setProperty(KeyStoreConstants.PROP_PUB_KS_URL,
clientKeyStoreURL.toExternalForm());
+ System.setProperty(KeyStoreConstants.PROP_PUB_KS_PWD, "clientpwd");
+ KeyStoreHelper.reInit();
+ }
+
+ @Test
+ void testDeserializationFilterBlocksBeforeStaticInitializer() throws
Exception {
+ byte[] probeBytes = serializeObject(new
testdata.enhancement.StaticInitProbe());
+
+ // Clear the marker set by the serialization above (loading the class
runs its static init)
+
System.clearProperty(testdata.enhancement.StaticInitProbe.MARKER_PROPERTY);
+
+ // Use a child-first classloader so Class.forName triggers a fresh
class load
+ // (the JVM only runs static initializers once per classloader)
+ ClassLoader childFirstCL = new
ChildFirstClassLoader(getClass().getClassLoader());
+
+ JavaSerializableResolverStrategy strategy =
+ new
JavaSerializableResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT);
+
+ assertThatThrownBy(() ->
+ strategy.unmarshal(null, null, probeBytes, childFirstCL))
+ .as("Deserialization of non-whitelisted class should be
rejected")
+ .isInstanceOf(RuntimeException.class);
+
+
assertThat(System.getProperty(testdata.enhancement.StaticInitProbe.MARKER_PROPERTY))
+ .as("Static initializer should NOT have run — the filter
should reject "
+ + "the class before Class.forName initializes it")
+ .isNull();
+ }
+
+ private static class ChildFirstClassLoader extends ClassLoader {
+ private final ClassLoader parent;
+
+ ChildFirstClassLoader(ClassLoader parent) {
+ super(parent);
+ this.parent = parent;
+ }
+
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
+ if (name.startsWith("testdata.enhancement.")) {
+ Class<?> loaded = findLoadedClass(name);
+ if (loaded != null) {
+ return loaded;
+ }
+ String resourceName = name.replace('.', '/') + ".class";
+ try (java.io.InputStream is =
parent.getResourceAsStream(resourceName)) {
+ if (is == null) {
+ throw new ClassNotFoundException(name);
+ }
+ byte[] bytes = is.readAllBytes();
+ return defineClass(name, bytes, 0, bytes.length);
+ } catch (IOException e) {
+ throw new ClassNotFoundException(name, e);
+ }
+ }
+ return super.loadClass(name, resolve);
+ }
+ }
+
+ private static byte[] serializeObject(Object obj) throws IOException {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos)) {
+ oos.writeObject(obj);
+ oos.flush();
+ return bos.toByteArray();
+ }
+ }
+
+ private static byte[] extractProtobufBytes(byte[] objectStreamBytes)
throws IOException {
+ try (ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(objectStreamBytes))) {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ byte[] buf = new byte[4096];
+ int read;
+ while ((read = ois.read(buf)) != -1) {
+ result.write(buf, 0, read);
+ }
+ return result.toByteArray();
+ }
+ }
+
+ private static byte[] wrapInObjectStream(byte[] protobufBytes) throws
IOException {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos)) {
+ oos.write(protobufBytes);
+ oos.flush();
+ return bos.toByteArray();
+ }
+ }
+}
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/QueryTest.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/QueryTest.java
index cea98f0db83..37e577e8e51 100644
---
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/QueryTest.java
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/QueryTest.java
@@ -42,6 +42,17 @@ import static org.assertj.core.api.Assertions.assertThat;
public class QueryTest extends CommonTestMethodBase {
+ private final DeserializationFilterTestSupport filterSupport = new
DeserializationFilterTestSupport();
+
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+ filterSupport.setUp("org.drools.mvel.compiler.Cheese");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+ filterSupport.tearDown();
+ }
private static QueryResults getQueryResults(KieSession session, String
queryName, Object... arguments ) throws Exception {
QueryResultsImpl results = (QueryResultsImpl) session.getQueryResults(
queryName, arguments );
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/SerializationHelper.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/SerializationHelper.java
index 694882c0df4..b52edc15fbb 100644
---
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/SerializationHelper.java
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/SerializationHelper.java
@@ -93,4 +93,25 @@ public class SerializationHelper {
return readSessionResult;
}
+
+ public static byte[] serializeStatefulKnowledgeSession(final KieSession
ksession) throws Exception {
+ final KieBase kbase = ksession.getKieBase();
+ final ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase,
+ (ObjectMarshallingStrategy[])
ksession.getEnvironment().get(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES));
+ ksession.getEnvironment().set(EnvironmentName.GLOBALS,
ksession.getGlobals());
+
+ try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ marshaller.marshall(bos, ksession,
ksession.getSessionClock().getCurrentTime());
+ return bos.toByteArray();
+ }
+ }
+
+ public static StatefulKnowledgeSession
deserializeStatefulKnowledgeSession(final byte[] serializedSession,
+
final KieBase kbase) throws Exception {
+ final ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase);
+
+ try (final ByteArrayInputStream bais = new
ByteArrayInputStream(serializedSession)) {
+ return marshaller.unmarshall(bais, null, null);
+ }
+ }
}
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/TruthMaintenanceTest.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/TruthMaintenanceTest.java
index fe16a6792c1..74ad801315f 100644
---
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/TruthMaintenanceTest.java
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/TruthMaintenanceTest.java
@@ -57,6 +57,22 @@ import static
org.drools.serialization.protobuf.SerializationHelper.getSerialise
public class TruthMaintenanceTest extends CommonTestMethodBase {
+ private final DeserializationFilterTestSupport filterSupport = new
DeserializationFilterTestSupport();
+
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+ filterSupport.setUp("org.drools.mvel.compiler.Address",
+ "org.drools.mvel.compiler.Alarm",
+ "org.drools.mvel.compiler.Cheese",
+ "org.drools.mvel.compiler.Person",
+ "org.drools.mvel.compiler.Sensor");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+ filterSupport.tearDown();
+ }
+
@Test
public void testLogicalInsertionsDynamicRule() throws Exception {
KnowledgeBuilder kbuilder =
KnowledgeBuilderFactory.newKnowledgeBuilder();
diff --git
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/UnmarshallingTest.java
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/UnmarshallingTest.java
index 52594eac9a9..686fc4a32ee 100644
---
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/UnmarshallingTest.java
+++
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/UnmarshallingTest.java
@@ -44,6 +44,18 @@ import static org.assertj.core.api.Assertions.fail;
public class UnmarshallingTest {
+ private final DeserializationFilterTestSupport filterSupport = new
DeserializationFilterTestSupport();
+
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+
filterSupport.setUp("org.drools.serialization.protobuf.UnmarshallingTest$Ben");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+ filterSupport.tearDown();
+ }
+
@Test
public void testMarshallWithNot() throws Exception {
String whenBenNotVilgaxRule =
diff --git
a/drools-serialization-protobuf/src/test/java/testdata/enhancement/DeserializationProbe.java
b/drools-serialization-protobuf/src/test/java/testdata/enhancement/DeserializationProbe.java
new file mode 100644
index 00000000000..101ab303142
--- /dev/null
+++
b/drools-serialization-protobuf/src/test/java/testdata/enhancement/DeserializationProbe.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 testdata.enhancement;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+
+public class DeserializationProbe implements Serializable {
+ private static final long serialVersionUID = 1L;
+ public static volatile boolean deserialized = false;
+
+ private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
+ in.defaultReadObject();
+ deserialized = true;
+ }
+}
diff --git
a/drools-serialization-protobuf/src/test/java/testdata/enhancement/EnumProbe.java
b/drools-serialization-protobuf/src/test/java/testdata/enhancement/EnumProbe.java
new file mode 100644
index 00000000000..a9437daa47e
--- /dev/null
+++
b/drools-serialization-protobuf/src/test/java/testdata/enhancement/EnumProbe.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 testdata.enhancement;
+
+public enum EnumProbe {
+ INSTANCE;
+
+ public static volatile boolean initialized = false;
+
+ static {
+ initialized = true;
+ }
+}
diff --git
a/drools-serialization-protobuf/src/test/java/testdata/enhancement/StaticInitProbe.java
b/drools-serialization-protobuf/src/test/java/testdata/enhancement/StaticInitProbe.java
new file mode 100644
index 00000000000..53c2567d5a1
--- /dev/null
+++
b/drools-serialization-protobuf/src/test/java/testdata/enhancement/StaticInitProbe.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 testdata.enhancement;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class StaticInitProbe implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ public static final String MARKER_PROPERTY =
"drools.test.staticInitProbe.initialized";
+
+ static {
+ System.setProperty(MARKER_PROPERTY, "true");
+ }
+}
diff --git a/drools-serialization-protobuf/src/test/resources/logback-test.xml
b/drools-serialization-protobuf/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..fb13562b853
--- /dev/null
+++ b/drools-serialization-protobuf/src/test/resources/logback-test.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+
+-->
+<configuration>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ <root level="WARN">
+ <appender-ref ref="STDOUT"/>
+ </root>
+</configuration>
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/BackwardChainingTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/BackwardChainingTest.java
index 15feda07449..8672ec5c22a 100644
---
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/BackwardChainingTest.java
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/BackwardChainingTest.java
@@ -88,6 +88,27 @@ import static org.kie.api.runtime.rule.Variable.v;
public class BackwardChainingTest extends AbstractBackwardChainingTest {
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+
System.setProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+
"org.drools.compiler.integrationtests.BackwardChainingTest$Man;" +
+
"org.drools.compiler.integrationtests.BackwardChainingTest$Woman;" +
+
"org.drools.compiler.integrationtests.BackwardChainingTest$Parent;" +
+ "org.drools.compiler.test.Door;" +
+ "org.drools.compiler.test.Edible;" +
+ "org.drools.compiler.test.Here;" +
+ "org.drools.compiler.test.Location;" +
+ "org.drools.compiler.test.Room;" +
+ "org.drools.compiler.test.TastesYucky;" +
+ "org.drools.testcoverage.common.model.Person;" +
+ "org.drools.testcoverage.common.model.Address");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+
System.clearProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+
public static Stream<KieBaseTestConfiguration> parameters() {
return
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
}
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/CepEspTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/CepEspTest.java
index ed56a00f5ca..1aea08d4f5f 100644
---
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/CepEspTest.java
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/CepEspTest.java
@@ -108,6 +108,21 @@ import static org.mockito.Mockito.verify;
public class CepEspTest extends AbstractCepEspTest {
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+
System.setProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+ "org.drools.compiler.integrationtests.CepEspTest$EventA;" +
+ "org.drools.compiler.integrationtests.CepEspTest$SuperClass;" +
+ "org.drools.compiler.integrationtests.CepEspTest$SubClass;" +
+ "org.drools.testcoverage.common.model.OrderEvent;" +
+ "org.drools.testcoverage.common.model.StockTick");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+
System.clearProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+
public static Stream<KieBaseTestConfiguration> parameters() {
return
TestParametersUtil2.getKieBaseStreamConfigurations(true).stream();
}
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/incrementalcompilation/IncrementalCompilationTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/incrementalcompilation/IncrementalCompilationTest.java
index 172f455a702..e907f6b0e2d 100644
---
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/incrementalcompilation/IncrementalCompilationTest.java
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/incrementalcompilation/IncrementalCompilationTest.java
@@ -101,6 +101,19 @@ import static
org.drools.core.util.DroolsTestUtil.rulestoMap;
public class IncrementalCompilationTest {
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+
System.setProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+ "org.drools.testcoverage.common.model.Message;" +
+ "org.drools.testcoverage.common.model.Person;" +
+ "org.drools.testcoverage.common.model.Address");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+
System.clearProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+
public static Stream<KieBaseTestConfiguration> parameters() {
return
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
}
diff --git
a/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/EventDeserializationInPastTest.java
b/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/EventDeserializationInPastTest.java
index bd11e5bceaf..4ad6a25567f 100644
---
a/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/EventDeserializationInPastTest.java
+++
b/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/EventDeserializationInPastTest.java
@@ -45,6 +45,17 @@ import static org.assertj.core.api.Assertions.fail;
*/
public class EventDeserializationInPastTest {
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+
System.setProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+
"org.drools.testcoverage.regression.EventDeserializationInPastTest$Event1");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+
System.clearProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+
@Test
public void testSerializationWithEventInPastBZ1205666() {
// DROOLS-749
diff --git
a/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/LogicalInsertionsSerializationTest.java
b/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/LogicalInsertionsSerializationTest.java
index a676607881f..7e22c487e91 100644
---
a/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/LogicalInsertionsSerializationTest.java
+++
b/drools-test-coverage/test-suite/src/test/java/org/drools/testcoverage/regression/LogicalInsertionsSerializationTest.java
@@ -50,6 +50,18 @@ public class LogicalInsertionsSerializationTest extends
KieSessionTest {
@TempDir
public File name;
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+
System.setProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+ "org.drools.testcoverage.regression.Person;" +
+ "org.drools.testcoverage.regression.Employee");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+
System.clearProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+
public static Stream<Arguments> parameters() {
return
TestParametersUtil2.getKieBaseAndStatefulKieSessionConfigurations().stream();
}
diff --git
a/drools-traits/src/test/java/org/drools/traits/compiler/factmodel/traits/LogicalTraitTest.java
b/drools-traits/src/test/java/org/drools/traits/compiler/factmodel/traits/LogicalTraitTest.java
index 365a818d2f2..096d99dd565 100644
---
a/drools-traits/src/test/java/org/drools/traits/compiler/factmodel/traits/LogicalTraitTest.java
+++
b/drools-traits/src/test/java/org/drools/traits/compiler/factmodel/traits/LogicalTraitTest.java
@@ -60,6 +60,21 @@ public class LogicalTraitTest extends CommonTraitTest {
private static final Logger LOGGER =
LoggerFactory.getLogger(LogicalTraitTest.class);
+ @org.junit.jupiter.api.BeforeEach
+ public void setUpDeserializationFilter() {
+ // Prefixes used because trait proxy class names are generated at
runtime from DRL-declared
+ // types (e.g. org.drools.test.X.org.drools.test.Y_Proxy). Exact names
are not practical
+ // as each test method declares different trait/core combinations.
+
System.setProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+ "org.drools.test.*;" +
+ "org.drools.factmodel.traits.*");
+ }
+
+ @org.junit.jupiter.api.AfterEach
+ public void clearDeserializationFilter() {
+
System.clearProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+ }
+
public static Stream<VirtualPropertyMode> parameters() {
return Stream.of(VirtualPropertyMode.MAP, VirtualPropertyMode.TRIPLES);
}
@@ -1096,6 +1111,77 @@ public class LogicalTraitTest extends CommonTraitTest {
}
}
+ @ParameterizedTest()
+ @MethodSource("parameters")
+ public void testSerialWithFreshKieBase(VirtualPropertyMode mode) throws
Exception {
+
+ String drl = "package org.drools.test; \n" +
+ "import org.drools.base.factmodel.traits.*; \n" +
+ "import org.drools.base.factmodel.traits.Trait; \n" +
+ "" +
+ "global java.util.List list; \n" +
+ "" +
+ "declare trait X end \n" +
+ "declare trait Z end \n" +
+ "" +
+ "declare Y \n" +
+ "@Traitable( ) \n" +
+ "end \n" +
+ "" +
+ "rule Don \n" +
+ "when \n" +
+ "then \n" +
+ " Y y = new Y( ); \n" +
+ " don( y, X.class ); \n" +
+ " don( y, Z.class ); \n" +
+ "end \n" +
+ "" +
+ "rule CheckTraits \n" +
+ "when \n" +
+ " String( this == \"go\" ) \n" +
+ " $x : X() \n" +
+ " $z : Z() \n" +
+ "then \n" +
+ " list.add( \"ok\" ); \n" +
+ "end \n" +
+ "";
+
+ KieBase kbase = loadKnowledgeBaseFromString( drl );
+ TraitFactoryImpl.setMode(mode, (InternalRuleBase) kbase);
+
+ KieSession ks = kbase.newKieSession();
+ List list = new ArrayList();
+ ks.setGlobal( "list", list );
+
+ ks.fireAllRules();
+
+ // Verify facts exist before serialization: 1 Y + 1 X proxy + 1 Z
proxy = 3
+ assertThat(ks.getObjects()).hasSize(3);
+
+ byte[] serialized =
SerializationHelper.serializeStatefulKnowledgeSession( ks );
+
+ ks.dispose();
+
+ // Build a fresh KieBase from the same DRL to simulate JVM restart
+ KieBase freshKbase = loadKnowledgeBaseFromString( drl );
+ TraitFactoryImpl.setMode(mode, (InternalRuleBase) freshKbase);
+
+ KieSession deserialized =
SerializationHelper.deserializeStatefulKnowledgeSession( serialized, freshKbase
);
+
+ // Verify facts survived serialization/deserialization
+ assertThat(deserialized.getObjects()).hasSize(3);
+
+ List freshList = new ArrayList();
+ deserialized.setGlobal( "list", freshList );
+
+ deserialized.insert( "go" );
+ deserialized.fireAllRules();
+
+ assertThat(freshList).containsExactly("ok");
+
+ deserialized.dispose();
+ }
+
@ParameterizedTest()
@MethodSource("parameters")
public void testTraitMismatchTypes(VirtualPropertyMode mode)
diff --git
a/drools-traits/src/test/java/org/drools/traits/compiler/factmodel/traits/TraitMarshallingValidationTest.java
b/drools-traits/src/test/java/org/drools/traits/compiler/factmodel/traits/TraitMarshallingValidationTest.java
new file mode 100644
index 00000000000..f41f215ed54
--- /dev/null
+++
b/drools-traits/src/test/java/org/drools/traits/compiler/factmodel/traits/TraitMarshallingValidationTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.drools.traits.compiler.factmodel.traits;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import com.google.protobuf.ByteString;
+import org.drools.core.impl.EnvironmentFactory;
+import org.drools.core.marshalling.ClassObjectMarshallingStrategyAcceptor;
+import org.drools.core.marshalling.SerializablePlaceholderResolverStrategy;
+import org.drools.kiesession.rulebase.InternalKnowledgeBase;
+import org.drools.serialization.protobuf.ProtobufMarshaller;
+import org.drools.serialization.protobuf.ProtobufMessages;
+import org.drools.wiring.api.classloader.ProjectClassLoader;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.io.ResourceType;
+import org.kie.api.marshalling.ObjectMarshallingStrategy;
+import org.kie.api.runtime.Environment;
+import org.kie.api.runtime.EnvironmentName;
+import org.kie.api.runtime.KieSession;
+import org.kie.internal.marshalling.MarshallerFactory;
+import org.kie.internal.utils.KieHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class TraitMarshallingValidationTest {
+
+ @Test
+ void testInjectedRuntimeClassDefLandsInStoreWhenTraitsPresent() throws
Exception {
+ String drl =
+ "package org.drools.test;\n" +
+ "declare MyFact\n" +
+ " value : String\n" +
+ "end\n" +
+ "rule R1 when\n" +
+ " String()\n" +
+ "then\n" +
+ "end\n";
+
+ KieBase kbase = new KieHelper().addContent(drl,
ResourceType.DRL).build();
+
+ ObjectMarshallingStrategy[] strategies = new
ObjectMarshallingStrategy[]{
+ new
SerializablePlaceholderResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT)
+ };
+ Environment env = EnvironmentFactory.newEnvironment();
+ env.set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, strategies);
+
+ KieSession ksession = kbase.newKieSession(null, env);
+ ksession.insert("test");
+ ksession.fireAllRules();
+
+ ProtobufMarshaller marshaller = (ProtobufMarshaller)
MarshallerFactory.newMarshaller(kbase, strategies);
+
+ byte[] serializedSession;
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ marshaller.marshall(bos, ksession,
ksession.getSessionClock().getCurrentTime());
+ serializedSession = bos.toByteArray();
+ }
+ ksession.dispose();
+
+ ProjectClassLoader pcl = (ProjectClassLoader) ((InternalKnowledgeBase)
kbase).getRootClassLoader();
+
+ byte[] protobufBytes = extractProtobufBytes(serializedSession);
+ ProtobufMessages.Header originalHeader =
ProtobufMessages.Header.parseFrom(protobufBytes);
+
+ String injectedClassName = "example/Payload.class";
+ byte[] fakeBytecode = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte)
0xBA, (byte) 0xBE};
+
+ ProtobufMessages.Header tamperedHeader = originalHeader.toBuilder()
+
.addRuntimeClassDefinitions(ProtobufMessages.RuntimeClassDef.newBuilder()
+ .setClassFqName(injectedClassName)
+ .setClassDef(ByteString.copyFrom(fakeBytecode))
+ .build())
+ .build();
+
+ byte[] tamperedSession =
wrapInObjectStream(tamperedHeader.toByteArray());
+
+ assertThatThrownBy(() ->
+ marshaller.unmarshall(new
ByteArrayInputStream(tamperedSession)))
+ .as("With drools-traits on the classpath, TraitFactory is
non-null, "
+ + "but readRuntimeDefinedClasses() should still reject
"
+ + "class names that don't match the trait proxy naming
pattern")
+ .isInstanceOf(RuntimeException.class);
+
+ assertThat(pcl.getStore() != null &&
pcl.getStore().containsKey(injectedClassName))
+ .as("Injected bytecode for '%s' should NOT be in the
classloader store",
+ injectedClassName)
+ .isFalse();
+ }
+
+ private static byte[] extractProtobufBytes(byte[] objectStreamBytes)
throws IOException {
+ try (ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(objectStreamBytes))) {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ byte[] buf = new byte[4096];
+ int read;
+ while ((read = ois.read(buf)) != -1) {
+ result.write(buf, 0, read);
+ }
+ return result.toByteArray();
+ }
+ }
+
+ private static byte[] wrapInObjectStream(byte[] protobufBytes) throws
IOException {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos)) {
+ oos.write(protobufBytes);
+ oos.flush();
+ return bos.toByteArray();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]