This is an automated email from the ASF dual-hosted git repository.

shuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new 789ae8e  Make it possible to white list and black list classes used in 
OGNL and MVEL2 scripting languages (#158)
789ae8e is described below

commit 789ae8e820c507866b9c91590feebffa4e996f5e
Author: Serge Huber <[email protected]>
AuthorDate: Tue May 12 10:00:23 2020 +0200

    Make it possible to white list and black list classes used in OGNL and 
MVEL2 scripting languages (#158)
    
    * Make it possible to white list and black list classes used in OGNL and 
MVEL2 scripting languages
    
    * Add new integration test to AllITs list
    
    * Add some documentation for the configuration parameters
---
 .../unomi/common/SecureFilteringClassLoader.java   | 112 +++++++++++++++++++
 .../test/java/org/apache/unomi/itests/AllITs.java  |   3 +-
 .../test/java/org/apache/unomi/itests/BaseIT.java  |  21 +++-
 .../java/org/apache/unomi/itests/SecurityIT.java   | 122 +++++++++++++++++++++
 manual/src/main/asciidoc/configuration.adoc        |  18 +++
 .../main/resources/etc/custom.system.properties    |   3 +-
 persistence-elasticsearch/core/pom.xml             |   6 +
 .../conditions/ConditionContextHelper.java         |  29 +++--
 plugins/baseplugin/pom.xml                         |   6 +
 .../conditions/PropertyConditionEvaluator.java     |  62 +++++++----
 .../conditions/PropertyConditionEvaluatorTest.java |  44 +++++++-
 pom.xml                                            |   2 +-
 services/pom.xml                                   |   7 ++
 .../services/actions/ActionExecutorDispatcher.java |  39 ++++---
 .../actions/ActionExecutorDispatcherTest.java      | 106 ++++++++++++++++++
 15 files changed, 521 insertions(+), 59 deletions(-)

diff --git 
a/common/src/main/java/org/apache/unomi/common/SecureFilteringClassLoader.java 
b/common/src/main/java/org/apache/unomi/common/SecureFilteringClassLoader.java
new file mode 100644
index 0000000..61b91bf
--- /dev/null
+++ 
b/common/src/main/java/org/apache/unomi/common/SecureFilteringClassLoader.java
@@ -0,0 +1,112 @@
+/*
+ * 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.apache.unomi.common;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A class loader that uses a whitelist and a black list of classes that it 
will allow to resolve. This is useful for providing proper
+ * sandboxing to scripting engine such as MVEL, OGNL or Groovy.
+ */
+public class SecureFilteringClassLoader extends ClassLoader {
+
+    private Set<String> allowedClasses = null;
+    private Set<String> forbiddenClasses = null;
+
+    private static Set<String> defaultAllowedClasses = null;
+    private static Set<String> defaultForbiddenClasses = null;
+
+    static {
+        String systemAllowedClasses = 
System.getProperty("org.apache.unomi.scripting.allow",
+                
"org.apache.unomi.api.Event,org.apache.unomi.api.Profile,org.apache.unomi.api.Session,org.apache.unomi.api.Item,org.apache.unomi.api.CustomItem,ognl.*,java.lang.Object,java.util.Map,java.lang.Integer,org.mvel2.*");
+        if (systemAllowedClasses != null) {
+            if ("all".equals(systemAllowedClasses.trim())) {
+                defaultAllowedClasses = null;
+            } else {
+                if (systemAllowedClasses.trim().length() > 0) {
+                    String[] systemAllowedClassesParts = 
systemAllowedClasses.split(",");
+                    defaultAllowedClasses = new HashSet<>();
+                    
defaultAllowedClasses.addAll(Arrays.asList(systemAllowedClassesParts));
+                } else {
+                    defaultAllowedClasses = null;
+                }
+            }
+        }
+
+        String systemForbiddenClasses = 
System.getProperty("org.apache.unomi.scripting.forbid", null);
+        if (systemForbiddenClasses != null) {
+            if (systemForbiddenClasses.trim().length() > 0) {
+                String[] systemForbiddenClassesParts = 
systemForbiddenClasses.split(",");
+                defaultForbiddenClasses = new HashSet<>();
+                
defaultForbiddenClasses.addAll(Arrays.asList(systemForbiddenClassesParts));
+            } else {
+                defaultForbiddenClasses = null;
+            }
+        }
+
+    }
+
+    ClassLoader delegate;
+
+    /**
+     * Sets up the securing filtering class loader, using the default allowed 
and forbidden classes. By default the
+     * @param delegate the class loader we delegate to if the filtering was 
not applied.
+     */
+    public SecureFilteringClassLoader(ClassLoader delegate) {
+        this(defaultAllowedClasses, defaultForbiddenClasses, delegate);
+    }
+
+    /**
+     * Sets up the secure filtering class loader
+     * @param allowedClasses the list of allowed FQN class names, or if this 
filtering is to be deactivated, pass null.
+     *                       if you want to allow no class, pass an empty 
hashset
+     * @param forbiddenClasses the list of forbidden FQN class names, or if 
this filtering is to be deactivated, pass null or an empty set
+     *
+     * @param delegate the class loader we delegate to if the filtering was 
not applied.
+     */
+    public SecureFilteringClassLoader(Set<String> allowedClasses, Set<String> 
forbiddenClasses, ClassLoader delegate) {
+        super(delegate);
+        this.allowedClasses = allowedClasses;
+        this.forbiddenClasses = forbiddenClasses;
+        this.delegate = delegate;
+    }
+
+    @Override
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        if (forbiddenClasses != null && classNameMatches(forbiddenClasses, 
name)) {
+            throw new ClassNotFoundException("Access to class " + name + " not 
allowed");
+        }
+        if (allowedClasses != null && !classNameMatches(allowedClasses, name)) 
{
+            throw new ClassNotFoundException("Access to class " + name + " not 
allowed");
+        }
+        return delegate.loadClass(name);
+    }
+
+    private boolean classNameMatches(Set<String> classesToTest, String 
className) {
+        for (String classToTest : classesToTest) {
+            if (classToTest.endsWith("*")) {
+                if (className.startsWith(classToTest.substring(0, 
classToTest.length() - 1))) return true;
+            } else {
+                if (className.equals(classToTest)) return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java 
b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index 938fcf8..c9ee4ff 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -41,7 +41,8 @@ import org.junit.runners.Suite.SuiteClasses;
        PropertiesUpdateActionIT.class,
        ModifyConsentIT.class,
        PatchIT.class,
-       UpdateEventFromContextServletIT.class
+       UpdateEventFromContextServletIT.class,
+       SecurityIT.class
 })
 public class AllITs {
 }
diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java 
b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index d1bdd33..6651562 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -69,7 +69,6 @@ public abstract class BaseIT {
         List<Option> options = new ArrayList<>();
 
         Option[] commonOptions = new Option[]{
-                debugConfiguration("5006", false),
                 karafDistributionConfiguration()
                         .frameworkUrl(karafUrl)
                         .unpackDirectory(new File(KARAF_DIR))
@@ -117,6 +116,26 @@ public abstract class BaseIT {
 
         options.addAll(Arrays.asList(commonOptions));
 
+        String karafDebug = System.getProperty("it.karaf.debug");
+        if (karafDebug != null) {
+            System.out.println("Found system Karaf Debug system property, 
activating configuration: " + karafDebug);
+            String port = "5006";
+            boolean hold = true;
+            if (karafDebug.trim().length() > 0) {
+                String[] debugOptions = karafDebug.split(",");
+                for (String debugOption : debugOptions) {
+                    String[] debugOptionParts = debugOption.split(":");
+                    if ("hold".equals(debugOptionParts[0])) {
+                        hold = 
Boolean.parseBoolean(debugOptionParts[1].trim());
+                    }
+                    if ("port".equals(debugOptionParts[0])) {
+                        port = debugOptionParts[1].trim();
+                    }
+                }
+            }
+            options.add(0, debugConfiguration(port, hold));
+        }
+
         if (JavaVersionUtil.getMajorVersion() >= 9) {
             Option[] jdk9PlusOptions = new Option[]{
                     new VMOption("--add-reads=java.xml=java.logging"),
diff --git a/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java 
b/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java
new file mode 100644
index 0000000..c2d5a3f
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/SecurityIT.java
@@ -0,0 +1,122 @@
+/*
+ * 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.apache.unomi.itests;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.unomi.api.ContextRequest;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.services.PersonalizationService;
+import org.apache.unomi.lifecycle.BundleWatcher;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.ops4j.pax.exam.util.Filter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class SecurityIT extends BaseIT {
+
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(SecurityIT.class);
+
+    private static final String SESSION_ID = "vuln-session-id";
+
+    private ObjectMapper objectMapper;
+    private  TestUtils testUtils = new TestUtils();
+
+    @Inject
+    @Filter(timeout = 600000)
+    protected BundleWatcher bundleWatcher;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        while (!bundleWatcher.isStartupComplete()) {
+            LOGGER.info("Waiting for startup to complete...");
+            Thread.sleep(1000);
+        }
+        objectMapper = CustomObjectMapper.getObjectMapper();
+    }
+
+    @Test
+    public void testOGNLInjection() throws IOException, InterruptedException {
+        ContextRequest contextRequest = new ContextRequest();
+        List<PersonalizationService.PersonalizationRequest> personalizations = 
new ArrayList<>();
+        PersonalizationService.PersonalizationRequest personalizationRequest = 
new PersonalizationService.PersonalizationRequest();
+        personalizationRequest.setId("vuln-test");
+        personalizationRequest.setStrategy("matching-first");
+        Map<String,Object> strategyOptions = new HashMap<>();
+        strategyOptions.put("fallback", "var2");
+        personalizationRequest.setStrategyOptions(strategyOptions);
+        List<PersonalizationService.PersonalizedContent> 
personalizationContents = new ArrayList<>();
+        PersonalizationService.PersonalizedContent var1Content = new 
PersonalizationService.PersonalizedContent();
+        var1Content.setId("var1");
+        List<PersonalizationService.Filter> filters = new ArrayList<>();
+        PersonalizationService.Filter filter = new 
PersonalizationService.Filter();
+        Condition condition = new Condition();
+        File vulnFile = new File("target/vuln-file.txt");
+        if (vulnFile.exists()) {
+            vulnFile.delete();
+        }
+        condition.setConditionTypeId("profilePropertyCondition");
+        condition.setParameter("propertyName", 
"@java.lang.Runtime@getRuntime().exec('touch " + vulnFile.getCanonicalPath() + 
"')");
+        condition.setParameter("comparisonOperator", "equals");
+        condition.setParameter("propertyValue" , "script::java.io.PrintWriter 
writer = new java.io.PrintWriter(new java.io.BufferedWriter(new 
java.io.FileWriter(\"" + vulnFile.getCanonicalPath() + "\", 
true)));\nwriter.println(\"test\");\nwriter.close();");
+        filter.setCondition(condition);
+        filters.add(filter);
+        var1Content.setFilters(filters);
+        personalizationContents.add(var1Content);
+        PersonalizationService.PersonalizedContent var2Content = new 
PersonalizationService.PersonalizedContent();
+        var2Content.setId("var2");
+        personalizationContents.add(var2Content);
+        personalizationRequest.setContents(personalizationContents);
+
+        personalizations.add(personalizationRequest);
+        contextRequest.setPersonalizations(personalizations);
+
+        contextRequest.setSessionId(SESSION_ID);
+        HttpPost request = new HttpPost(URL + "/context.json");
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.create("application/json")));
+
+        TestUtils.RequestResponse response = 
executeContextJSONRequest(request, SESSION_ID);
+        assertFalse("Vulnerability successfully executed ! File created at " + 
vulnFile.getCanonicalPath(), vulnFile.exists());
+    }
+
+    private TestUtils.RequestResponse executeContextJSONRequest(HttpPost 
request, String sessionId) throws IOException {
+        return testUtils.executeContextJSONRequest(request, sessionId);
+    }
+
+
+
+}
diff --git a/manual/src/main/asciidoc/configuration.adoc 
b/manual/src/main/asciidoc/configuration.adoc
index e191fa8..688d8ad 100644
--- a/manual/src/main/asciidoc/configuration.adoc
+++ b/manual/src/main/asciidoc/configuration.adoc
@@ -193,6 +193,24 @@ You should now have SSL setup on Karaf with your 
certificate, and you can test i
 Changing the default Karaf password can be done by modifying the 
`org.apache.unomi.security.root.password` in the
 `$MY_KARAF_HOME/etc/unomi.custom.system.properties` file
 
+==== Scripting security
+
+By default, scripting (using in conditions, segments and rules) is controlled 
by a custom classloader that is quite
+restrictive and using a white-list/black list system. It is controlled through 
the following property in the
+`unomi.custom.system.properties` file:
+
+[source]
+----
+org.apache.unomi.scripting.allow=${env:UNOMI_ALLOW_SCRIPTING_CLASSES:-org.apache.unomi.api.Event,org.apache.unomi.api.Profile,org.apache.unomi.api.Session,org.apache.unomi.api.Item,org.apache.unomi.api.CustomItem,ognl.*,java.lang.Object,java.util.Map,java.lang.Integer,org.mvel2.*}
+org.apache.unomi.scripting.forbid=${env:UNOMI_FORBID_SCRIPTING_CLASSES:-}
+----
+
+If you encounter any errors while trying to access a class in a condition or 
an action it might be due to this
+restrictive configuration.
+
+If you need, for example when adding a custom item type, to adjust these, 
please be careful as scripts may be called
+directly from the context.json personalization conditions and therefore should 
be kept minimal.
+
 ==== Automatic profile merging
 
 Apache Unomi is capable of merging profiles based on a common property value. 
In order to use this, you must
diff --git a/package/src/main/resources/etc/custom.system.properties 
b/package/src/main/resources/etc/custom.system.properties
index 423adaa..bd23640 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -31,7 +31,8 @@ 
org.apache.unomi.hazelcast.network.port=${env:UNOMI_HAZELCAST_NETWORK_PORT:-5701
 ## Security settings                                                           
                                      ##
 
#######################################################################################################################
 org.apache.unomi.security.root.password=${env:UNOMI_ROOT_PASSWORD:-karaf}
-
+org.apache.unomi.scripting.allow=${env:UNOMI_ALLOW_SCRIPTING_CLASSES:-org.apache.unomi.api.Event,org.apache.unomi.api.Profile,org.apache.unomi.api.Session,org.apache.unomi.api.Item,org.apache.unomi.api.CustomItem,ognl.*,java.lang.Object,java.util.Map,java.lang.Integer,org.mvel2.*}
+org.apache.unomi.scripting.forbid=${env:UNOMI_FORBID_SCRIPTING_CLASSES:-}
 
#######################################################################################################################
 ## HTTP Settings                                                               
                                      ##
 
#######################################################################################################################
diff --git a/persistence-elasticsearch/core/pom.xml 
b/persistence-elasticsearch/core/pom.xml
index 5b9d569..9a59713 100644
--- a/persistence-elasticsearch/core/pom.xml
+++ b/persistence-elasticsearch/core/pom.xml
@@ -50,6 +50,12 @@
         </dependency>
         <dependency>
             <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-common</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
             <artifactId>unomi-persistence-spi</artifactId>
             <version>${project.version}</version>
             <scope>provided</scope>
diff --git 
a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionContextHelper.java
 
b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionContextHelper.java
index d6b18b4..8355015 100644
--- 
a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionContextHelper.java
+++ 
b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionContextHelper.java
@@ -24,6 +24,7 @@ import org.apache.logging.log4j.core.util.IOUtils;
 import org.apache.lucene.analysis.charfilter.MappingCharFilterFactory;
 import org.apache.lucene.analysis.util.ClasspathResourceLoader;
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.common.SecureFilteringClassLoader;
 import org.mvel2.MVEL;
 import org.mvel2.ParserConfiguration;
 import org.mvel2.ParserContext;
@@ -34,10 +35,7 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.Serializable;
 import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class ConditionContextHelper {
@@ -80,12 +78,7 @@ public class ConditionContextHelper {
                     return context.get(StringUtils.substringAfter(s, 
"parameter::"));
                 } else if (s.startsWith("script::")) {
                     String script = StringUtils.substringAfter(s, "script::");
-                    if (!mvelExpressions.containsKey(script)) {
-                        ParserConfiguration parserConfiguration = new 
ParserConfiguration();
-                        
parserConfiguration.setClassLoader(ConditionContextHelper.class.getClassLoader());
-                        
mvelExpressions.put(script,MVEL.compileExpression(script, new 
ParserContext(parserConfiguration)));
-                    }
-                    return MVEL.executeExpression(mvelExpressions.get(script), 
context);
+                    return executeScript(context, script);
                 }
             }
         } else if (value instanceof Map) {
@@ -111,6 +104,22 @@ public class ConditionContextHelper {
         return value;
     }
 
+    private static Object executeScript(Map<String, Object> context, String 
script) {
+        final ClassLoader tccl = 
Thread.currentThread().getContextClassLoader();
+        try {
+            if (!mvelExpressions.containsKey(script)) {
+                ClassLoader secureFilteringClassLoader = new 
SecureFilteringClassLoader(ConditionContextHelper.class.getClassLoader());
+                
Thread.currentThread().setContextClassLoader(secureFilteringClassLoader);
+                ParserConfiguration parserConfiguration = new 
ParserConfiguration();
+                parserConfiguration.setClassLoader(secureFilteringClassLoader);
+                mvelExpressions.put(script, MVEL.compileExpression(script, new 
ParserContext(parserConfiguration)));
+            }
+            return MVEL.executeExpression(mvelExpressions.get(script), 
context);
+        } finally {
+            Thread.currentThread().setContextClassLoader(tccl);
+        }
+    }
+
     private static boolean hasContextualParameter(Object value) {
         if (value instanceof String) {
             if (((String) value).startsWith("parameter::") || ((String) 
value).startsWith("script::")) {
diff --git a/plugins/baseplugin/pom.xml b/plugins/baseplugin/pom.xml
index 0e61331..cdf74d5 100644
--- a/plugins/baseplugin/pom.xml
+++ b/plugins/baseplugin/pom.xml
@@ -78,6 +78,12 @@
             <artifactId>jackson-databind</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-common</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
 
         <!-- Unit tests -->
         <dependency>
diff --git 
a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
 
b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
index 2b5d7f1..24466c0 100644
--- 
a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
+++ 
b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
@@ -23,6 +23,7 @@ import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.unomi.api.*;
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.common.SecureFilteringClassLoader;
 import 
org.apache.unomi.persistence.elasticsearch.conditions.ConditionContextHelper;
 import 
org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator;
 import 
org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher;
@@ -325,8 +326,10 @@ public class PropertyConditionEvaluator implements 
ConditionEvaluator {
     }
 
     protected Object getOGNLPropertyValue(Item item, String expression) throws 
Exception {
-        ExpressionAccessor accessor = getPropertyAccessor(item, expression);
-        return accessor != null ? accessor.get(getOgnlContext(), item) : null;
+        ClassLoader secureFilteringClassLoader = new 
SecureFilteringClassLoader(PropertyConditionEvaluator.class.getClassLoader());
+        OgnlContext ognlContext = getOgnlContext(secureFilteringClassLoader);
+        ExpressionAccessor accessor = getPropertyAccessor(item, expression, 
ognlContext, secureFilteringClassLoader);
+        return accessor != null ? accessor.get(ognlContext, item) : null;
     }
 
     private Object getNestedPropertyValue(String expressionPart, Map<String, 
Object> properties) {
@@ -337,35 +340,48 @@ public class PropertyConditionEvaluator implements 
ConditionEvaluator {
             if (mapValue == null) {
                 return null;
             }
-            String nextExpression = expressionPart.substring(nextDotPos+1);
-            return getNestedPropertyValue(nextExpression, (Map<String,Object>) 
mapValue);
+            String nextExpression = expressionPart.substring(nextDotPos + 1);
+            return getNestedPropertyValue(nextExpression, (Map<String, 
Object>) mapValue);
         } else {
             return properties.get(expressionPart);
         }
     }
 
-    private OgnlContext getOgnlContext() {
-        return (OgnlContext) Ognl.createDefaultContext(null, new 
MemberAccess() {
-            @Override
-            public Object setup(Map context, Object target, Member member, 
String propertyName) {
-                return null;
-            }
+    private class ClassLoaderClassResolver extends DefaultClassResolver {
+        private ClassLoader classLoader;
 
-            @Override
-            public void restore(Map context, Object target, Member member, 
String propertyName, Object state) {
+        public ClassLoaderClassResolver(ClassLoader classLoader) {
+            this.classLoader = classLoader;
+        }
 
-            }
+        @Override
+        protected Class toClassForName(String className) throws 
ClassNotFoundException {
+            return Class.forName(className, true, classLoader);
+        }
+    }
 
-            @Override
-            public boolean isAccessible(Map context, Object target, Member 
member, String propertyName) {
-                int modifiers = member.getModifiers();
-                boolean result = Modifier.isPublic(modifiers);
-                return result;
-            }
-        });
+    private OgnlContext getOgnlContext(ClassLoader classLoader) {
+        return (OgnlContext) Ognl.createDefaultContext(null, new 
MemberAccess() {
+                    @Override
+                    public Object setup(Map context, Object target, Member 
member, String propertyName) {
+                        return null;
+                    }
+
+                    @Override
+                    public void restore(Map context, Object target, Member 
member, String propertyName, Object state) {
+                    }
+
+                    @Override
+                    public boolean isAccessible(Map context, Object target, 
Member member, String propertyName) {
+                        int modifiers = member.getModifiers();
+                        boolean result = Modifier.isPublic(modifiers);
+                        return result;
+                    }
+                }, new ClassLoaderClassResolver(classLoader),
+                null);
     }
 
-    private ExpressionAccessor getPropertyAccessor(Item item, String 
expression) throws Exception {
+    private ExpressionAccessor getPropertyAccessor(Item item, String 
expression, OgnlContext ognlContext, ClassLoader classLoader) throws Exception {
         ExpressionAccessor accessor = null;
         String clazz = item.getClass().getName();
         Map<String, ExpressionAccessor> expressions = 
expressionCache.get(clazz);
@@ -380,8 +396,8 @@ public class PropertyConditionEvaluator implements 
ConditionEvaluator {
             Thread current = Thread.currentThread();
             ClassLoader contextCL = current.getContextClassLoader();
             try {
-                
current.setContextClassLoader(PropertyConditionEvaluator.class.getClassLoader());
-                Node node = Ognl.compileExpression(getOgnlContext() , item, 
expression);
+                current.setContextClassLoader(classLoader);
+                Node node = Ognl.compileExpression(ognlContext, item, 
expression);
                 accessor = node.getAccessor();
             } finally {
                 current.setContextClassLoader(contextCL);
diff --git 
a/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
 
b/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
index fefab83..dff90fb 100644
--- 
a/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
+++ 
b/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
@@ -16,12 +16,14 @@
  */
 package org.apache.unomi.plugins.baseplugin.conditions;
 
+import ognl.MethodFailedException;
 import org.apache.unomi.api.CustomItem;
 import org.apache.unomi.api.Event;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.junit.Test;
 
+import java.io.File;
 import java.util.*;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
@@ -31,6 +33,7 @@ import java.util.concurrent.Future;
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertNull;
 import static 
org.apache.unomi.plugins.baseplugin.conditions.PropertyConditionEvaluator.NOT_OPTIMIZED_MARKER;
+import static org.junit.Assert.assertFalse;
 
 public class PropertyConditionEvaluatorTest {
 
@@ -52,14 +55,14 @@ public class PropertyConditionEvaluatorTest {
         assertEquals("Target itemId value is not correct", MOCK_ITEM_ID, 
propertyConditionEvaluator.getHardcodedPropertyValue(mockEvent, 
"target.itemId"));
         assertEquals("Target scope is not correct", DIGITALL_SCOPE, 
propertyConditionEvaluator.getHardcodedPropertyValue(mockEvent, 
"target.scope"));
         assertEquals("Target page path value is not correct", PAGE_PATH_VALUE, 
propertyConditionEvaluator.getHardcodedPropertyValue(mockEvent, 
"target.properties.pageInfo.pagePath"));
-        assertEquals("Target page url value is not correct", PAGE_URL_VALUE,   
              propertyConditionEvaluator.getHardcodedPropertyValue(mockEvent, 
"target.properties.pageInfo.pageURL"));
+        assertEquals("Target page url value is not correct", PAGE_URL_VALUE, 
propertyConditionEvaluator.getHardcodedPropertyValue(mockEvent, 
"target.properties.pageInfo.pageURL"));
         assertEquals("Session size should be 10", 10, 
propertyConditionEvaluator.getHardcodedPropertyValue(mockSession, "size"));
 
         assertNull("Unexisting property should be null", 
propertyConditionEvaluator.getHardcodedPropertyValue(mockSession, 
"systemProperties.goals._csk6r4cgeStartReached"));
         assertNull("Unexisting property should be null", 
propertyConditionEvaluator.getHardcodedPropertyValue(mockProfile, 
"properties.email"));
 
         // here let's make sure our reporting of non optimized expressions 
works.
-        assertEquals("Should have received the non-optimized marker string", 
NOT_OPTIMIZED_MARKER, 
propertyConditionEvaluator.getHardcodedPropertyValue(mockSession, 
"lastEventDate") );
+        assertEquals("Should have received the non-optimized marker string", 
NOT_OPTIMIZED_MARKER, 
propertyConditionEvaluator.getHardcodedPropertyValue(mockSession, 
"lastEventDate"));
 
     }
 
@@ -67,11 +70,11 @@ public class PropertyConditionEvaluatorTest {
     public void testOGNLEvaluator() throws Exception {
         Event mockEvent = generateMockEvent();
         assertEquals("Target itemId value is not correct", MOCK_ITEM_ID, 
propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, "target.itemId"));
-        assertEquals("Target scope is not correct", 
DIGITALL_SCOPE,propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, 
"target.scope"));
+        assertEquals("Target scope is not correct", DIGITALL_SCOPE, 
propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, "target.scope"));
         assertEquals("Target page path value is not correct", PAGE_PATH_VALUE, 
propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, 
"target.properties.pageInfo.pagePath"));
         assertEquals("Target page url value is not correct", PAGE_URL_VALUE, 
propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, 
"target.properties.pageInfo.pageURL"));
         assertEquals("Session size should be 10", 10, 
propertyConditionEvaluator.getOGNLPropertyValue(mockSession, "size"));
-        assertEquals("Should have received the proper last even date", 
SESSION_LAST_EVENT_DATE, 
propertyConditionEvaluator.getOGNLPropertyValue(mockSession, "lastEventDate") );
+        assertEquals("Should have received the proper last even date", 
SESSION_LAST_EVENT_DATE, 
propertyConditionEvaluator.getOGNLPropertyValue(mockSession, "lastEventDate"));
 
         assertNull("Unexisting property should be null", 
propertyConditionEvaluator.getHardcodedPropertyValue(mockSession, 
"systemProperties.goals._csk6r4cgeStartReached"));
         assertNull("Unexisting property should be null", 
propertyConditionEvaluator.getHardcodedPropertyValue(mockProfile, 
"properties.email"));
@@ -97,7 +100,7 @@ public class PropertyConditionEvaluatorTest {
     public void testPropertyEvaluator() throws Exception {
         Event mockEvent = generateMockEvent();
         assertEquals("Target itemId value is not correct", MOCK_ITEM_ID, 
propertyConditionEvaluator.getPropertyValue(mockEvent, "target.itemId"));
-        assertEquals("Target scope is not correct", 
DIGITALL_SCOPE,propertyConditionEvaluator.getPropertyValue(mockEvent, 
"target.scope"));
+        assertEquals("Target scope is not correct", DIGITALL_SCOPE, 
propertyConditionEvaluator.getPropertyValue(mockEvent, "target.scope"));
         assertEquals("Target page path value is not correct", PAGE_PATH_VALUE, 
propertyConditionEvaluator.getPropertyValue(mockEvent, 
"target.properties.pageInfo.pagePath"));
 
         assertNull("Unexisting property should be null", 
propertyConditionEvaluator.getPropertyValue(mockSession, 
"systemProperties.goals._csk6r4cgeStartReached"));
@@ -107,6 +110,35 @@ public class PropertyConditionEvaluatorTest {
         assertEquals("Session last event date is not right", 
SESSION_LAST_EVENT_DATE, 
propertyConditionEvaluator.getPropertyValue(mockSession, "lastEventDate"));
     }
 
+    @Test
+    public void testOGNLSecurity() throws Exception {
+        Event mockEvent = generateMockEvent();
+        File vulnFile = new File("target/vuln-file.txt");
+        if (vulnFile.exists()) {
+            vulnFile.delete();
+        }
+        try {
+            propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, 
"@java.lang.Runtime@getRuntime().exec('touch " + vulnFile.getCanonicalPath() + 
"')");
+        } catch (RuntimeException | MethodFailedException re) {
+            // we ignore these exceptions as they are expected.
+        }
+        assertFalse("Vulnerability successfully executed ! File created at " + 
vulnFile.getCanonicalPath(), vulnFile.exists());
+        try {
+            propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, 
"(#cmd='touch " + vulnFile.getCanonicalPath() + 
"').(#cmds={'bash','-c',#cmd}).(#p=new 
java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start())");
+        } catch (RuntimeException | MethodFailedException re) {
+            // we ignore these exceptions as they are expected.
+        }
+        vulnFile = new File("target/vuln-file.txt");
+        assertFalse("Vulnerability successfully executed ! File created at " + 
vulnFile.getCanonicalPath(), vulnFile.exists());
+        try {
+            propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, 
"(#cmd='touch " + vulnFile.getCanonicalPath() + 
"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'bash','-c',#cmd})).(#p=new
 
java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start())");
+        } catch (RuntimeException | MethodFailedException re) {
+            // we ignore these exceptions as they are expected.
+        }
+        vulnFile = new File("target/vuln-file.txt");
+        assertFalse("Vulnerability successfully executed ! File created at " + 
vulnFile.getCanonicalPath(), vulnFile.exists());
+    }
+
     private void runHardcodedTest(int workerCount, ExecutorService 
executorService) throws InterruptedException {
         List<Callable<Object>> todo = new 
ArrayList<Callable<Object>>(workerCount);
         long startTime = System.currentTimeMillis();
@@ -135,7 +167,7 @@ public class PropertyConditionEvaluatorTest {
         targetItem.setItemId(MOCK_ITEM_ID);
         targetItem.setScope(DIGITALL_SCOPE);
         mockEvent.setTarget(targetItem);
-        Map<String,Object> pageInfoMap = new HashMap<>();
+        Map<String, Object> pageInfoMap = new HashMap<>();
         pageInfoMap.put("pagePath", PAGE_PATH_VALUE);
         pageInfoMap.put("pageURL", PAGE_URL_VALUE);
         targetItem.getProperties().put("pageInfo", pageInfoMap);
diff --git a/pom.xml b/pom.xml
index 8ebfaac..f05734e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -971,7 +971,7 @@
             <dependency>
                 <groupId>ognl</groupId>
                 <artifactId>ognl</artifactId>
-                <version>3.2.11</version>
+                <version>3.2.14</version>
             </dependency>
             <dependency>
                 <groupId>org.javassist</groupId>
diff --git a/services/pom.xml b/services/pom.xml
index 7ca0b9a..ca4e1bf 100644
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -134,6 +134,13 @@
         </dependency>
 
         <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-common</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
             <groupId>com.github.seancfoley</groupId>
             <artifactId>ipaddress</artifactId>
             <version>4.3.0</version>
diff --git 
a/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java
 
b/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java
index 91a9125..e148a94 100644
--- 
a/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java
+++ 
b/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java
@@ -24,6 +24,7 @@ import org.apache.unomi.api.actions.Action;
 import org.apache.unomi.api.actions.ActionDispatcher;
 import org.apache.unomi.api.actions.ActionExecutor;
 import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.common.SecureFilteringClassLoader;
 import org.apache.unomi.metrics.MetricAdapter;
 import org.apache.unomi.metrics.MetricsService;
 import org.mvel2.MVEL;
@@ -98,23 +99,9 @@ public class ActionExecutorDispatcher {
         valueExtractors.put("script", new ValueExtractor() {
             @Override
             public Object extract(String valueAsString, Event event) throws 
IllegalAccessException, NoSuchMethodException, InvocationTargetException {
-                final ClassLoader tccl = 
Thread.currentThread().getContextClassLoader();
-                try {
-                    
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
-                    if (!mvelExpressions.containsKey(valueAsString)) {
-                        ParserConfiguration parserConfiguration = new 
ParserConfiguration();
-                        
parserConfiguration.setClassLoader(getClass().getClassLoader());
-                        mvelExpressions.put(valueAsString, 
MVEL.compileExpression(valueAsString, new ParserContext(parserConfiguration)));
-                    }
-                    Map<String, Object> ctx = new HashMap<>();
-                    ctx.put("event", event);
-                    ctx.put("session", event.getSession());
-                    ctx.put("profile", event.getProfile());
-                    return 
MVEL.executeExpression(mvelExpressions.get(valueAsString), ctx);
-                } finally {
-                    Thread.currentThread().setContextClassLoader(tccl);
-                }
+                return executeScript(valueAsString, event);
             }
+
         });
     }
 
@@ -239,4 +226,24 @@ public class ActionExecutorDispatcher {
         }
     }
 
+    protected Object executeScript(String valueAsString, Event event) {
+        final ClassLoader tccl = 
Thread.currentThread().getContextClassLoader();
+        try {
+            ClassLoader secureFilteringClassLoader = new 
SecureFilteringClassLoader(getClass().getClassLoader());
+            
Thread.currentThread().setContextClassLoader(secureFilteringClassLoader);
+            if (!mvelExpressions.containsKey(valueAsString)) {
+                ParserConfiguration parserConfiguration = new 
ParserConfiguration();
+                parserConfiguration.setClassLoader(secureFilteringClassLoader);
+                mvelExpressions.put(valueAsString, 
MVEL.compileExpression(valueAsString, new ParserContext(parserConfiguration)));
+            }
+            Map<String, Object> ctx = new HashMap<>();
+            ctx.put("event", event);
+            ctx.put("session", event.getSession());
+            ctx.put("profile", event.getProfile());
+            return MVEL.executeExpression(mvelExpressions.get(valueAsString), 
ctx);
+        } finally {
+            Thread.currentThread().setContextClassLoader(tccl);
+        }
+    }
+
 }
diff --git 
a/services/src/test/java/org/apache/unomi/services/actions/ActionExecutorDispatcherTest.java
 
b/services/src/test/java/org/apache/unomi/services/actions/ActionExecutorDispatcherTest.java
new file mode 100644
index 0000000..813c067
--- /dev/null
+++ 
b/services/src/test/java/org/apache/unomi/services/actions/ActionExecutorDispatcherTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.apache.unomi.services.actions;
+
+import org.apache.unomi.api.CustomItem;
+import org.apache.unomi.api.Event;
+import org.apache.unomi.common.SecureFilteringClassLoader;
+import org.junit.Test;
+import org.mvel2.CompileException;
+import org.mvel2.MVEL;
+import org.mvel2.ParserConfiguration;
+import org.mvel2.ParserContext;
+
+import java.io.*;
+import java.util.*;
+
+import static org.junit.Assert.assertFalse;
+
+public class ActionExecutorDispatcherTest {
+
+    public static final String MOCK_ITEM_ID = "mockItemId";
+    public static final String DIGITALL_SCOPE = "digitall";
+    public static final String PAGE_PATH_VALUE = "/site/en/home/aboutus.html";
+    public static final String PAGE_URL_VALUE = 
"http://localhost:8080/site/en/home/aboutus.html";;
+
+    @Test
+    public void testMVELSecurity() throws IOException {
+        Map<String, Object> ctx = new HashMap<>();
+        Event mockEvent = generateMockEvent();
+        ctx.put("event", mockEvent);
+        ctx.put("session", mockEvent.getSession());
+        ctx.put("profile", mockEvent.getProfile());
+        File vulnFile = new File("target/vuln-file.txt");
+        if (vulnFile.exists()) {
+            vulnFile.delete();
+        }
+        Object result = null;
+        try {
+            result = executeMVEL("java.io.PrintWriter writer = new 
java.io.PrintWriter(new java.io.BufferedWriter(new java.io.FileWriter(\"" + 
vulnFile.getCanonicalPath() + "\", 
true)));\nwriter.println(\"test\");\nwriter.close();", ctx);
+        } catch (CompileException ce) {
+            // this is expected since access to these classes should not be 
allowed
+        }
+        System.out.println("result=" + result);
+        try {
+            result = executeMVEL("import java.util.*;\nimport 
java.io.*;\nPrintWriter writer = new PrintWriter(new BufferedWriter(new 
FileWriter(\"" + vulnFile.getCanonicalPath() + "\", 
true)));\nwriter.println(\"test\");\nwriter.close();", ctx);
+        } catch (CompileException ce) {
+            // this is expected since access to these classes should not be 
allowed
+        }
+        System.out.println("result=" + result);
+        try {
+            result = executeMVEL("import java.util.*;\nimport java.io.*;\nnew 
Scanner(new File(\"" + vulnFile.getCanonicalPath() + 
"\")).useDelimiter(\"\\\\Z\").next();", ctx);
+        } catch (CompileException ce) {
+            // this is expected since access to these classes should not be 
allowed
+        }
+        System.out.println("result=" + result);
+        assertFalse("Vulnerability successfully executed ! File created at " + 
vulnFile.getCanonicalPath(), vulnFile.exists());
+    }
+
+    private Object executeMVEL(String expression, Object ctx) {
+        final ClassLoader tccl = 
Thread.currentThread().getContextClassLoader();
+        try {
+            ParserConfiguration parserConfiguration = new 
ParserConfiguration();
+            ClassLoader secureFilteringClassLoader = new 
SecureFilteringClassLoader(getClass().getClassLoader());
+            
Thread.currentThread().setContextClassLoader(secureFilteringClassLoader);
+            parserConfiguration.setClassLoader(secureFilteringClassLoader);
+            ParserContext parserContext = new 
ParserContext(parserConfiguration);
+            Serializable compiledExpression = 
MVEL.compileExpression(expression, parserContext);
+            try {
+                return MVEL.executeExpression(compiledExpression, ctx, new 
HashMap());
+            } catch (CompileException ce) {
+                // this is expected
+            }
+            return null;
+        } finally {
+            Thread.currentThread().setContextClassLoader(tccl);
+        }
+    }
+
+    private static Event generateMockEvent() {
+        Event mockEvent = new Event();
+        CustomItem targetItem = new CustomItem();
+        targetItem.setItemId(MOCK_ITEM_ID);
+        targetItem.setScope(DIGITALL_SCOPE);
+        mockEvent.setTarget(targetItem);
+        Map<String, Object> pageInfoMap = new HashMap<>();
+        pageInfoMap.put("pagePath", PAGE_PATH_VALUE);
+        pageInfoMap.put("pageURL", PAGE_URL_VALUE);
+        targetItem.getProperties().put("pageInfo", pageInfoMap);
+        return mockEvent;
+    }
+
+}

Reply via email to