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

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


The following commit(s) were added to refs/heads/unomi-1.4.x by this push:
     new 3d15022  Make it possible to white list and black list classes used in 
OGNL and MVEL2 scripting languages (#158)
3d15022 is described below

commit 3d15022e4b52a2fcc0912ef6c259c3905d4f374a
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
    
    (cherry picked from commit 789ae8e820c507866b9c91590feebffa4e996f5e)
---
 .../unomi/common/SecureFilteringClassLoader.java   | 112 ++++++++++++++++
 .../test/java/org/apache/unomi/itests/AllITs.java  |   5 +-
 .../java/org/apache/unomi/itests/SecurityIT.java   | 122 +++++++++++++++++
 .../java/org/apache/unomi/itests/TestUtils.java    |  82 +++++++++---
 manual/src/main/asciidoc/configuration.adoc        |  18 +++
 .../main/resources/etc/custom.system.properties    |   3 +-
 .../main/resources/etc/org.ops4j.pax.url.mvn.cfg   | 146 +++++++++++++++++++++
 persistence-elasticsearch/core/pom.xml             |   6 +
 .../conditions/ConditionContextHelper.java         |  29 ++--
 plugins/baseplugin/pom.xml                         |   6 +
 .../conditions/PropertyConditionEvaluator.java     |  58 ++++++--
 .../conditions/PropertyConditionEvaluatorTest.java |  44 ++++++-
 pom.xml                                            |   2 +-
 services/pom.xml                                   |   7 +
 .../services/actions/ActionExecutorDispatcher.java |  39 +++---
 .../actions/ActionExecutorDispatcherTest.java      | 106 +++++++++++++++
 16 files changed, 720 insertions(+), 65 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 5456a30..42d4b7d 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -23,7 +23,7 @@ import org.junit.runners.Suite.SuiteClasses;
 
 /**
  * Defines suite of test classes to run.
- * 
+ *
  * @author Sergiy Shyrkov
  */
 @RunWith(Suite.class)
@@ -40,7 +40,8 @@ import org.junit.runners.Suite.SuiteClasses;
         ProfileExportIT.class,
         PropertiesUpdateActionIT.class,
         ModifyConsentIT.class,
-        PatchIT.class
+        PatchIT.class,
+               SecurityIT.class
 })
 public class AllITs {
 }
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/itests/src/test/java/org/apache/unomi/itests/TestUtils.java 
b/itests/src/test/java/org/apache/unomi/itests/TestUtils.java
index 7cc9733..066adbd 100644
--- a/itests/src/test/java/org/apache/unomi/itests/TestUtils.java
+++ b/itests/src/test/java/org/apache/unomi/itests/TestUtils.java
@@ -19,29 +19,75 @@ package org.apache.unomi.itests;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.ContextResponse;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.junit.Assert;
 
 import java.io.IOException;
 
 public class TestUtils {
+       private static final String JSON_MYME_TYPE = "application/json";
 
-    public static <T> T retrieveResourceFromResponse(HttpResponse response, 
Class<T> clazz) throws IOException {
-        if (response == null) {
-            return null;
-        }
-        if (response.getEntity() == null) {
-            return null;
-        }
-        String jsonFromResponse = EntityUtils.toString(response.getEntity());
-        // ObjectMapper mapper = new 
ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
-        ObjectMapper mapper = CustomObjectMapper.getObjectMapper();
-        try {
-            T value = mapper.readValue(jsonFromResponse, clazz);
-            return value;
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-        return null;
-    }
+       public static <T> T retrieveResourceFromResponse(HttpResponse response, 
Class<T> clazz) throws IOException {
+               if (response == null) {
+                       return null;
+               }
+               if (response.getEntity() == null) {
+                       return null;
+               }
+               String jsonFromResponse = 
EntityUtils.toString(response.getEntity());
+               // ObjectMapper mapper = new 
ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+               ObjectMapper mapper = CustomObjectMapper.getObjectMapper();
+               try {
+                       T value = mapper.readValue(jsonFromResponse, clazz);
+                       return value;
+               } catch (Throwable t) {
+                       t.printStackTrace();
+               }
+               return null;
+       }
+
+       public RequestResponse executeContextJSONRequest(HttpPost request, 
String sessionId) throws IOException {
+               try (CloseableHttpResponse response = 
HttpClientBuilder.create().build().execute(request)) {
+                       // validate mimeType
+                       String mimeType = 
ContentType.getOrDefault(response.getEntity()).getMimeType();
+                       Assert.assertEquals("Response content type should be " 
+ JSON_MYME_TYPE, JSON_MYME_TYPE, mimeType);
+
+                       // validate context
+                       ContextResponse context = 
TestUtils.retrieveResourceFromResponse(response, ContextResponse.class);
+                       Assert.assertNotNull("Context should not be null", 
context);
+                       Assert.assertNotNull("Context profileId should not be 
null", context.getProfileId());
+                       Assert.assertEquals("Context sessionId should be the 
same as the sessionId used to request the context", sessionId,
+                               context.getSessionId());
+
+                       String cookieHeader = null;
+                       if (response.containsHeader("Set-Cookie")) {
+                               cookieHeader = 
response.getHeaders("Set-Cookie")[0].toString().substring(12);
+                       }
+                       return new RequestResponse(context, cookieHeader);
+               }
+       }
+
+       public static class RequestResponse {
+               private ContextResponse contextResponse;
+               private String cookieHeaderValue;
+
+               public RequestResponse(ContextResponse contextResponse, String 
cookieHeaderValue) {
+                       this.contextResponse = contextResponse;
+                       this.cookieHeaderValue = cookieHeaderValue;
+               }
+
+               public ContextResponse getContextResponse() {
+                       return contextResponse;
+               }
+
+               public String getCookieHeaderValue() {
+                       return cookieHeaderValue;
+               }
+       }
 }
diff --git a/manual/src/main/asciidoc/configuration.adoc 
b/manual/src/main/asciidoc/configuration.adoc
index 81a04d0..4e65016 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 f1ad51e..1342313 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/package/src/main/resources/etc/org.ops4j.pax.url.mvn.cfg 
b/package/src/main/resources/etc/org.ops4j.pax.url.mvn.cfg
new file mode 100644
index 0000000..801642c
--- /dev/null
+++ b/package/src/main/resources/etc/org.ops4j.pax.url.mvn.cfg
@@ -0,0 +1,146 @@
+################################################################################
+#
+#    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.
+#
+################################################################################
+
+#
+# see: https://ops4j1.jira.com/wiki/display/paxurl/Aether+Configuration
+#
+
+# If set to true, the following property will not allow any certificate to be 
used
+# when accessing Maven repositories through SSL
+#
+org.ops4j.pax.url.mvn.certificateCheck=true
+
+#
+# Path to the local Maven settings file.
+# The repositories defined in this file will be automatically added to the list
+# of default repositories if the 'org.ops4j.pax.url.mvn.repositories' property
+# below is not set.
+# The following locations are checked for the existence of the settings.xml 
file
+#   * 1. looks for the specified url
+#   * 2. if not found looks for ${user.home}/.m2/settings.xml
+#   * 3. if not found looks for ${maven.home}/conf/settings.xml
+#   * 4. if not found looks for ${M2_HOME}/conf/settings.xml
+#
+# Properties prefixed with "org.ops4j.pax.url.mvn." have
+# higher priority except <proxies> element. HTTP proxies should be configured 
in
+# settings file
+#org.ops4j.pax.url.mvn.settings=
+
+#
+# Path to the local Maven repository which is used to avoid downloading
+# artifacts when they already exist locally.
+# The value of this property will be extracted from the settings.xml file
+# above, or defaulted to:
+#     System.getProperty( "user.home" ) + "/.m2/repository"
+# leaving this option commented makes the system dependent on external
+# configuration, which is not always desired
+# "localRepository" is the target location for artifacts downloaded from
+# "remote repositories"
+#org.ops4j.pax.url.mvn.localRepository=
+
+#
+# Default this to false. It's just weird to use undocumented repos
+# "false" means that http://repo1.maven.org/maven2@id=central won't be
+# implicitly used as remote repository
+#
+org.ops4j.pax.url.mvn.useFallbackRepositories=false
+
+#
+# Comma separated list of repositories scanned when resolving an artifact.
+# list of repositories searched in the first place, should contain
+# ${runtime.home}/${karaf.default.repository}.
+# if "org.ops4j.pax.url.mvn.localRepository" is defined and it's not
+# ~/.m2/repository, it's recommended (at least for dev purposes) to add
+# ~/.m2/repository to defaultRepositories
+# each of these repositories is checked by aether as "local repository". if
+# artifact isn't found, "repositories" are searched next
+#
+# Those repositories will be checked before iterating through the
+#    below list of repositories and even before the local repository
+# A repository url can be appended with zero or more of the following flags:
+#    @snapshots  : the repository contains snaphots
+#    @noreleases : the repository does not contain any released artifacts
+#
+# The following property value will add the system folder as a repo.
+#
+org.ops4j.pax.url.mvn.defaultRepositories=\
+    
${karaf.home.uri}${karaf.default.repository}@id=system.repository@snapshots, \
+    ${karaf.data.uri}kar@id=kar.repository@multi@snapshots, \
+    
${karaf.base.uri}${karaf.default.repository}@id=child.system.repository@snapshots
+
+#
+# if "defaultLocalRepoAsRemote" is set do *any* value, localRepository will be
+# added to the list of remote repositories being searched for artifacts
+#
+#org.ops4j.pax.url.mvn.defaultLocalRepoAsRemote = true
+
+#
+# Comma separated list of repositories scanned when resolving an artifact.
+# list of repositories searched after resolution fails for 
"defaultRepositories"
+# These are true remote repositories accessed using maven/aether/wagon
+# mechanisms. If any repository contains required artifact, it is then written
+# to "localRepository"
+#
+# if this list is _prepended_ with '+' sign, all repositories from active
+# profiles defined in effective settings.xml file will be _appended_ to this
+# list
+# The default list includes the following repositories:
+#    http://repo1.maven.org/maven2@id=central
+#    
http://repository.apache.org/content/groups/snapshots-group@id=apache@snapshots@noreleases
+#    
https://oss.sonatype.org/content/repositories/snapshots@id=sonatype.snapshots.deploy@snapshots@noreleases
+#    
https://oss.sonatype.org/content/repositories/ops4j-snapshots@id=ops4j.sonatype.snapshots.deploy@snapshots@noreleases
+# A repository url can be appended with zero or more of the following flags:
+#    @snapshots  : the repository contains snapshots
+#    @noreleases : the repository does not contain any released artifacts
+#    @id=repository.id : the id for the repository, just like in the
+#        settings.xml this is optional but recommended
+#
+org.ops4j.pax.url.mvn.repositories= \
+    https://repo1.maven.org/maven2@id=central, \
+    
https://repository.apache.org/content/groups/snapshots-group@id=apache@snapshots@noreleases,
 \
+    
https://oss.sonatype.org/content/repositories/ops4j-snapshots@id=ops4j.sonatype.snapshots.deploy@snapshots@noreleases
+
+#
+# Global policies override repository-specific settings (@checksum=..., 
@update=..., @releasesUpdate=..., ...)
+#
+#org.ops4j.pax.url.mvn.globalUpdatePolicy = daily
+#org.ops4j.pax.url.mvn.globalChecksumPolicy = warn
+
+#
+# socket and connection configuration (pax-url-aether 2.5.0)
+#
+# default value for connection and read timeouts, when socket.readTimeout and 
socket.connectionTimeout
+# are not specified
+org.ops4j.pax.url.mvn.timeout = 5000
+# timeout in ms when establishing http connection during artifact resolution
+org.ops4j.pax.url.mvn.socket.connectionTimeout = 5000
+# timeout in ms when reading data after connecting to remote repository
+org.ops4j.pax.url.mvn.socket.readTimeout = 30000
+# SO_KEEPALIVE option for sockets, defaults to false
+org.ops4j.pax.url.mvn.socket.keepAlive = false
+# SO_LINGER option for sockets, defaults to -1
+org.ops4j.pax.url.mvn.socket.linger = -1
+# SO_REUSEADDR option for sockets, defaults to false
+org.ops4j.pax.url.mvn.socket.reuseAddress = false
+# TCP_NODELAY option for sockets, defaults to true
+org.ops4j.pax.url.mvn.socket.tcpNoDelay = true
+# Configure buffer size for HTTP connections (output and input buffers), 
defaults to 8192 bytes
+org.ops4j.pax.url.mvn.connection.bufferSize = 8192
+# Number of connection retries after failure is detected in http client. 
httpclient uses default value "3"
+org.ops4j.pax.url.mvn.connection.retryCount = 3
diff --git a/persistence-elasticsearch/core/pom.xml 
b/persistence-elasticsearch/core/pom.xml
index 1745547..b62a426 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 d6279bb..f35fe97 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 239c321..3b8da80 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
@@ -17,15 +17,13 @@
 
 package org.apache.unomi.plugins.baseplugin.conditions;
 
-import ognl.Node;
-import ognl.Ognl;
-import ognl.OgnlContext;
-import ognl.OgnlException;
+import ognl.*;
 import ognl.enhance.ExpressionAccessor;
 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;
@@ -36,6 +34,8 @@ import org.elasticsearch.index.mapper.DateFieldMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.lang.reflect.Member;
+import java.lang.reflect.Modifier;
 import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.function.LongSupplier;
@@ -327,8 +327,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((OgnlContext) 
Ognl.createDefaultContext(null), 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) {
@@ -339,14 +341,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 ExpressionAccessor getPropertyAccessor(Item item, String 
expression) throws Exception {
+    private class ClassLoaderClassResolver extends DefaultClassResolver {
+        private ClassLoader classLoader;
+
+        public ClassLoaderClassResolver(ClassLoader classLoader) {
+            this.classLoader = classLoader;
+        }
+
+        @Override
+        protected Class toClassForName(String className) throws 
ClassNotFoundException {
+            return Class.forName(className, true, classLoader);
+        }
+    }
+
+    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, OgnlContext ognlContext, ClassLoader classLoader) throws Exception {
         ExpressionAccessor accessor = null;
         String clazz = item.getClass().getName();
         Map<String, ExpressionAccessor> expressions = 
expressionCache.get(clazz);
@@ -361,8 +397,8 @@ public class PropertyConditionEvaluator implements 
ConditionEvaluator {
             Thread current = Thread.currentThread();
             ClassLoader contextCL = current.getContextClassLoader();
             try {
-                
current.setContextClassLoader(PropertyConditionEvaluator.class.getClassLoader());
-                Node node = Ognl.compileExpression((OgnlContext) 
Ognl.createDefaultContext(null), 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 db8493f..a67de95 100644
--- a/pom.xml
+++ b/pom.xml
@@ -891,7 +891,7 @@
             <dependency>
                 <groupId>ognl</groupId>
                 <artifactId>ognl</artifactId>
-                <version>3.0.11</version>
+                <version>3.2.14</version>
             </dependency>
             <dependency>
                 <groupId>javassist</groupId>
diff --git a/services/pom.xml b/services/pom.xml
index f081a98..87b46d2 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 6306be6..0a0d951 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
@@ -23,6 +23,7 @@ import org.apache.unomi.api.Event;
 import org.apache.unomi.api.actions.Action;
 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;
@@ -89,23 +90,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);
             }
+
         });
     }
 
@@ -202,4 +189,24 @@ public class ActionExecutorDispatcher {
         Object extract(String valueAsString, Event event) throws 
IllegalAccessException, NoSuchMethodException, InvocationTargetException;
     }
 
+    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