You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2020/11/04 14:17:31 UTC
[unomi] 01/02: Improve scripting security (#179)
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
commit 7279419fa2e74ec5d04cb3c57439c26d408315bb
Author: Serge Huber <sh...@jahia.com>
AuthorDate: Thu Aug 20 15:07:40 2020 +0200
Improve scripting security (#179)
* Improve scripting security:
- OGNL is now disabled by default as it wasn't really used much (possible to reactive through a system setting)
- A new condition sanitizer has been added in the ContextServlet to filter out any MVEL scripts (again not used much and can be reactivate using a system property)
- A new ExpressionFilter has been added that will use configurable (system property) regular expressions to filter out possible malicious expressions
- OGNL sandboxing has been improved
- MVEL default imports have been modified to prevent using system-level classes
* Fix bug in sanitizing code
* New scripting execution sub-system:
- Allow-listing of allowed expressions
- Plugins may deployed their own allow-lists using JSON files
- OGNL scripting is now deactivated by default
- Minimal list of built-in MVEL allowed patterns
* Fix typo in marker
(cherry picked from commit 343d24f27b1790701225711491e967bd3e2111c8)
---
common/pom.xml | 36 ++
.../test/java/org/apache/unomi/itests/BaseIT.java | 58 +++
.../org/apache/unomi/itests/ContextServletIT.java | 390 +++++++++++++++++++++
.../test/resources/security/mvel-payload-1.json | 20 ++
.../test/resources/security/ognl-payload-1.json | 28 ++
kar/pom.xml | 5 +
kar/src/main/feature/feature.xml | 1 +
.../main/resources/etc/custom.system.properties | 7 +-
persistence-elasticsearch/core/pom.xml | 11 +-
.../conditions/ConditionContextHelper.java | 42 +--
.../ConditionESQueryBuilderDispatcher.java | 10 +-
.../conditions/ConditionEvaluatorDispatcher.java | 8 +-
.../resources/OSGI-INF/blueprint/blueprint.xml | 3 +
plugins/baseplugin/pom.xml | 6 +
.../PastEventConditionESQueryBuilder.java | 8 +-
.../conditions/PastEventConditionEvaluator.java | 9 +-
.../conditions/PropertyConditionEvaluator.java | 116 +++++-
.../resources/META-INF/cxs/expressions/mvel.json | 13 +
.../resources/OSGI-INF/blueprint/blueprint.xml | 3 +
.../conditions/PropertyConditionEvaluatorTest.java | 34 +-
pom.xml | 3 +-
{plugins/baseplugin => scripting}/pom.xml | 87 ++---
.../apache/unomi/scripting/ExpressionFilter.java | 59 ++++
.../unomi/scripting/ExpressionFilterFactory.java | 31 ++
.../apache/unomi/scripting/MvelScriptExecutor.java | 80 +++++
.../org/apache/unomi/scripting/ScriptExecutor.java | 28 ++
.../scripting}/SecureFilteringClassLoader.java | 22 +-
.../internal/ExpressionFilterFactoryImpl.java | 190 ++++++++++
.../resources/META-INF/cxs/expressions/mvel.json | 1 +
.../resources/META-INF/cxs/expressions/ognl.json | 1 +
.../resources/OSGI-INF/blueprint/blueprint.xml | 48 +++
.../unomi/scripting/MvelScriptExecutorTest.java | 130 +++++++
services/pom.xml | 12 +-
.../services/actions/ActionExecutorDispatcher.java | 43 +--
.../resources/OSGI-INF/blueprint/blueprint.xml | 5 +-
.../actions/ActionExecutorDispatcherTest.java | 106 ------
.../resources/OSGI-INF/blueprint/blueprint.xml | 1 +
.../java/org/apache/unomi/web/ContextServlet.java | 93 ++++-
38 files changed, 1496 insertions(+), 252 deletions(-)
diff --git a/common/pom.xml b/common/pom.xml
index 6623fe9..80201d3 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -54,6 +54,42 @@
<version>${version.karaf}</version>
<scope>provided</scope>
</dependency>
+
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-api</artifactId>
+ <version>2.0.0-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>1.6.6</version>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
+ <Import-Package>
+ org.apache.unomi.api,
+ org.apache.unomi.api.conditions,
+ org.apache.unomi.api.goals,
+ *
+ </Import-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
</project>
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 039fb51..d84660a 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -17,6 +17,13 @@
package org.apache.unomi.itests;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.IOUtils;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.persistence.spi.PersistenceService;
import org.junit.Assert;
import org.ops4j.pax.exam.Configuration;
import org.ops4j.pax.exam.CoreOptions;
@@ -24,8 +31,14 @@ import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel;
import org.ops4j.pax.exam.options.MavenArtifactUrlReference;
import org.ops4j.pax.exam.options.MavenUrlReference;
+import org.osgi.framework.BundleContext;
import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -45,6 +58,30 @@ public abstract class BaseIT {
protected static final String KARAF_DIR = "target/exam";
protected static final String UNOMI_KEY = "670c26d1cc413346c3b2fd9ce65dab41";
+ @Inject
+ @Filter(timeout = 600000)
+ protected PersistenceService persistenceService;
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected DefinitionsService definitionsService;
+
+ @Inject
+ protected BundleContext bundleContext;
+
+ protected void removeItems(final Class<? extends Item> ...classes) throws InterruptedException {
+ Condition condition = new Condition(definitionsService.getConditionType("matchAllCondition"));
+ for (Class<? extends Item> aClass : classes) {
+ persistenceService.removeByQuery(condition, aClass);
+ }
+ refreshPersistence();
+ }
+
+ protected void refreshPersistence() throws InterruptedException {
+ persistenceService.refresh();
+ Thread.sleep(1000);
+ }
+
@Configuration
public Option[] config() throws InterruptedException {
@@ -121,4 +158,25 @@ public abstract class BaseIT {
}
return value;
}
+
+ protected String bundleResourceAsString(final String resourcePath) throws IOException {
+ final java.net.URL url = bundleContext.getBundle().getResource(resourcePath);
+ if (url != null) {
+ return IOUtils.toString(url);
+ } else {
+ return null;
+ }
+ }
+
+ protected String getValidatedBundleJSON(final String resourcePath, Map<String,String> parameters) throws IOException {
+ String jsonString = bundleResourceAsString(resourcePath);
+ if (parameters != null && parameters.size() > 0) {
+ for (Map.Entry<String,String> parameterEntry : parameters.entrySet()) {
+ jsonString = jsonString.replace("###" + parameterEntry.getKey() + "###", parameterEntry.getValue());
+ }
+ }
+ ObjectMapper objectMapper = CustomObjectMapper.getObjectMapper();
+ return objectMapper.writeValueAsString(objectMapper.readTree(jsonString));
+ }
+
}
diff --git a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
new file mode 100644
index 0000000..812f36c
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
@@ -0,0 +1,390 @@
+/*
+ * 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.*;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.segments.Segment;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.api.services.SegmentService;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.junit.After;
+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 javax.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.hamcrest.core.IsCollectionContaining.hasItem;
+import static org.junit.Assert.*;
+
+
+/**
+ * Created by Ron Barabash on 5/4/2020.
+ */
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class ContextServletIT extends BaseIT {
+ private final static String CONTEXT_URL = "/context.json";
+ private final static String THIRD_PARTY_HEADER_NAME = "X-Unomi-Peer";
+ private final static String SEGMENT_EVENT_TYPE = "test-event-type";
+ private final static String SEGMENT_ID = "test-segment-id";
+ private final static int SEGMENT_NUMBER_OF_DAYS = 30;
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected EventService eventService;
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected PersistenceService persistenceService;
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected ProfileService profileService;
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected DefinitionsService definitionsService;
+
+ @Inject
+ @Filter(timeout = 600000)
+ protected SegmentService segmentService;
+
+ private Profile profile;
+
+ @Before
+ public void setUp() throws InterruptedException {
+ //Create a past-event segment
+ Metadata segmentMetadata = new Metadata(SEGMENT_ID);
+ Segment segment = new Segment(segmentMetadata);
+ Condition segmentCondition = new Condition(definitionsService.getConditionType("pastEventCondition"));
+ segmentCondition.setParameter("minimumEventCount",2);
+ segmentCondition.setParameter("numberOfDays",SEGMENT_NUMBER_OF_DAYS);
+ Condition pastEventEventCondition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
+ pastEventEventCondition.setParameter("eventTypeId",SEGMENT_EVENT_TYPE);
+ segmentCondition.setParameter("eventCondition",pastEventEventCondition);
+ segment.setCondition(segmentCondition);
+ segmentService.setSegmentDefinition(segment);
+
+ String profileId = "test-profile-id";
+ profile = new Profile(profileId);
+ profileService.save(profile);
+
+ refreshPersistence();
+ }
+
+ @After
+ public void tearDown() {
+ TestUtils.removeAllEvents(definitionsService, persistenceService);
+ TestUtils.removeAllSessions(definitionsService, persistenceService);
+ TestUtils.removeAllProfiles(definitionsService, persistenceService);
+ profileService.delete(profile.getItemId(), false);
+ segmentService.removeSegmentDefinition(SEGMENT_ID,false);
+ }
+
+ @Test
+ public void testUpdateEventFromContextAuthorizedThirdParty_Success() throws IOException, InterruptedException {
+ //Arrange
+ String eventId = "test-event-id1";
+ String profileId = "test-profile-id";
+ String sessionId = "test-session-id";
+ String scope = "test-scope";
+ String eventTypeOriginal = "test-event-type-original";
+ String eventTypeUpdated = "test-event-type-updated";
+ Profile profile = new Profile(profileId);
+ Session session = new Session(sessionId, profile, new Date(), scope);
+ Event event = new Event(eventId, eventTypeOriginal, session, profile, scope, null, null, new Date());
+ profileService.save(profile);
+ this.eventService.send(event);
+ refreshPersistence();
+ Thread.sleep(2000);
+ event.setEventType(eventTypeUpdated); //change the event so we can see the update effect
+
+ //Act
+ ContextRequest contextRequest = new ContextRequest();
+ contextRequest.setSessionId(session.getItemId());
+ contextRequest.setEvents(Arrays.asList(event));
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.addHeader(THIRD_PARTY_HEADER_NAME, UNOMI_KEY);
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json")));
+ TestUtils.executeContextJSONRequest(request, sessionId);
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+
+ //Assert
+ event = this.eventService.getEvent(eventId);
+ assertEquals(2, event.getVersion().longValue());
+ assertEquals(eventTypeUpdated,event.getEventType());
+ }
+
+ @Test
+ public void testUpdateEventFromContextUnAuthorizedThirdParty_Fail() throws IOException, InterruptedException {
+ //Arrange
+ String eventId = "test-event-id2";
+ String profileId = "test-profile-id";
+ String sessionId = "test-session-id";
+ String scope = "test-scope";
+ String eventTypeOriginal = "test-event-type-original";
+ String eventTypeUpdated = "test-event-type-updated";
+ Profile profile = new Profile(profileId);
+ Session session = new Session(sessionId, profile, new Date(), scope);
+ Event event = new Event(eventId, eventTypeOriginal, session, profile, scope, null, null, new Date());
+ profileService.save(profile);
+ this.eventService.send(event);
+ refreshPersistence();
+ Thread.sleep(2000);
+ event.setEventType(eventTypeUpdated); //change the event so we can see the update effect
+
+ //Act
+ ContextRequest contextRequest = new ContextRequest();
+ contextRequest.setSessionId(session.getItemId());
+ contextRequest.setEvents(Arrays.asList(event));
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json")));
+ TestUtils.executeContextJSONRequest(request, sessionId);
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+
+ //Assert
+ event = this.eventService.getEvent(eventId);
+ assertEquals(1, event.getVersion().longValue());
+ assertEquals(eventTypeOriginal,event.getEventType());
+ }
+
+
+ @Test
+ public void testUpdateEventFromContextAuthorizedThirdPartyNoItemID_Fail() throws IOException, InterruptedException {
+ //Arrange
+ String eventId = "test-event-id3";
+ String sessionId = "test-session-id";
+ String scope = "test-scope";
+ String eventTypeOriginal = "test-event-type-original";
+ String eventTypeUpdated = "test-event-type-updated";
+ Session session = new Session(sessionId, profile, new Date(), scope);
+ Event event = new Event(eventId, eventTypeOriginal, session, profile, scope, null, null, new Date());
+ this.eventService.send(event);
+ refreshPersistence();
+ Thread.sleep(2000);
+ event.setEventType(eventTypeUpdated); //change the event so we can see the update effect
+
+ //Act
+ ContextRequest contextRequest = new ContextRequest();
+ contextRequest.setSessionId(session.getItemId());
+ contextRequest.setEvents(Arrays.asList(event));
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json")));
+ TestUtils.executeContextJSONRequest(request, sessionId);
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+
+ //Assert
+ event = this.eventService.getEvent(eventId);
+ assertEquals(1, event.getVersion().longValue());
+ assertEquals(eventTypeOriginal,event.getEventType());
+ }
+
+ @Test
+ public void testCreateEventsWithNoTimestampParam_profileAddedToSegment() throws IOException, InterruptedException {
+ //Arrange
+ String sessionId = "test-session-id";
+ String scope = "test-scope";
+ Event event = new Event();
+ event.setEventType(SEGMENT_EVENT_TYPE);
+ event.setScope(scope);
+
+ //Act
+ ContextRequest contextRequest = new ContextRequest();
+ contextRequest.setSessionId(sessionId);
+ contextRequest.setRequireSegments(true);
+ contextRequest.setEvents(Arrays.asList(event));
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json")));
+ String cookieHeaderValue = TestUtils.executeContextJSONRequest(request, sessionId).getCookieHeaderValue();
+ refreshPersistence();
+ Thread.sleep(1000); //Making sure DB is updated
+
+ //Add the context-profile-id cookie to the second event
+ request.addHeader("Cookie", cookieHeaderValue);
+ ContextResponse response = (TestUtils.executeContextJSONRequest(request, sessionId)).getContextResponse(); //second event
+
+ refreshPersistence();
+
+ //Assert
+ assertEquals(1, response.getProfileSegments().size());
+ assertThat(response.getProfileSegments(),hasItem(SEGMENT_ID));
+ }
+
+ @Test
+ public void testCreateEventWithTimestampParam_pastEvent_profileIsNotAddedToSegment() throws IOException, InterruptedException {
+ //Arrange
+ String sessionId = "test-session-id";
+ String scope = "test-scope";
+ Event event = new Event();
+ event.setEventType(SEGMENT_EVENT_TYPE);
+ event.setScope(scope);
+ String regularURI = URL + CONTEXT_URL;
+ long oldTimestamp = LocalDateTime.now(ZoneId.of("UTC")).minusDays(SEGMENT_NUMBER_OF_DAYS + 1).toInstant(ZoneOffset.UTC).toEpochMilli();
+ String customTimestampURI = regularURI + "?timestamp=" + oldTimestamp;
+
+ //Act
+ ContextRequest contextRequest = new ContextRequest();
+ contextRequest.setSessionId(sessionId);
+ contextRequest.setRequireSegments(true);
+ contextRequest.setEvents(Arrays.asList(event));
+ HttpPost request = new HttpPost(regularURI);
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json")));
+ //The first event is with a default timestamp (now)
+ String cookieHeaderValue = TestUtils.executeContextJSONRequest(request, sessionId).getCookieHeaderValue();
+ refreshPersistence();
+ //The second event is with a customized timestamp
+ request.setURI(URI.create(customTimestampURI));
+ request.addHeader("Cookie", cookieHeaderValue);
+ ContextResponse response = (TestUtils.executeContextJSONRequest(request, sessionId)).getContextResponse(); //second event
+ refreshPersistence();
+
+ //Assert
+ assertEquals(0,response.getProfileSegments().size());
+ }
+
+ @Test
+ public void testCreateEventWithTimestampParam_futureEvent_profileIsNotAddedToSegment() throws IOException, InterruptedException {
+ //Arrange
+ String sessionId = "test-session-id";
+ String scope = "test-scope";
+ Event event = new Event();
+ event.setEventType(SEGMENT_EVENT_TYPE);
+ event.setScope(scope);
+ String regularURI = URL + CONTEXT_URL;
+ long futureTimestamp = LocalDateTime.now(ZoneId.of("UTC")).plusDays(1).toInstant(ZoneOffset.UTC).toEpochMilli();
+ String customTimestampURI = regularURI + "?timestamp=" + futureTimestamp;
+
+ //Act
+ ContextRequest contextRequest = new ContextRequest();
+ contextRequest.setSessionId(sessionId);
+ contextRequest.setRequireSegments(true);
+ contextRequest.setEvents(Arrays.asList(event));
+ HttpPost request = new HttpPost(regularURI);
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json")));
+ //The first event is with a default timestamp (now)
+ String cookieHeaderValue = TestUtils.executeContextJSONRequest(request, sessionId).getCookieHeaderValue();
+ refreshPersistence();
+ //The second event is with a customized timestamp
+ request.setURI(URI.create(customTimestampURI));
+ request.addHeader("Cookie", cookieHeaderValue);
+ ContextResponse response = (TestUtils.executeContextJSONRequest(request, sessionId)).getContextResponse(); //second event
+ refreshPersistence();
+
+ //Assert
+ assertEquals(0,response.getProfileSegments().size());
+ }
+
+ @Test
+ public void testCreateEventWithProfileId_Success() throws IOException, InterruptedException {
+ //Arrange
+ String eventId = "test-event-id4";
+ String profileId = "test-profile-id";
+ String eventType = "test-event-type";
+ Event event = new Event();
+ event.setEventType(eventType);
+ event.setItemId(eventId);
+
+ ContextRequest contextRequest = new ContextRequest();
+ contextRequest.setProfileId(profileId);
+ contextRequest.setEvents(Arrays.asList(event));
+
+ //Act
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.addHeader(THIRD_PARTY_HEADER_NAME, UNOMI_KEY);
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(contextRequest), ContentType.create("application/json")));
+ TestUtils.executeContextJSONRequest(request);
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+
+ //Assert
+ Profile profile = this.profileService.load(profileId);
+ assertEquals(profileId, profile.getItemId());
+ }
+
+ @Test
+ public void testOGNLVulnerability() throws IOException, InterruptedException {
+
+ File vulnFile = new File("target/vuln-file.txt");
+ if (vulnFile.exists()) {
+ vulnFile.delete();
+ }
+ String vulnFileCanonicalPath = vulnFile.getCanonicalPath();
+ vulnFileCanonicalPath = vulnFileCanonicalPath.replace("\\", "\\\\"); // this is required for Windows support
+
+ Map<String,String> parameters = new HashMap<>();
+ parameters.put("VULN_FILE_PATH", vulnFileCanonicalPath);
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.setEntity(new StringEntity(getValidatedBundleJSON("security/ognl-payload-1.json", parameters), ContentType.create("application/json")));
+ TestUtils.executeContextJSONRequest(request);
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+
+ assertFalse("Vulnerability successfully executed ! File created at " + vulnFileCanonicalPath, vulnFile.exists());
+
+ }
+
+ @Test
+ public void testMVELVulnerability() throws IOException, InterruptedException {
+
+ File vulnFile = new File("target/vuln-file.txt");
+ if (vulnFile.exists()) {
+ vulnFile.delete();
+ }
+ String vulnFileCanonicalPath = vulnFile.getCanonicalPath();
+ vulnFileCanonicalPath = vulnFileCanonicalPath.replace("\\", "\\\\"); // this is required for Windows support
+
+ Map<String,String> parameters = new HashMap<>();
+ parameters.put("VULN_FILE_PATH", vulnFileCanonicalPath);
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.setEntity(new StringEntity(getValidatedBundleJSON("security/mvel-payload-1.json", parameters), ContentType.create("application/json")));
+ TestUtils.executeContextJSONRequest(request);
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+
+ assertFalse("Vulnerability successfully executed ! File created at " + vulnFileCanonicalPath, vulnFile.exists());
+
+ }
+}
diff --git a/itests/src/test/resources/security/mvel-payload-1.json b/itests/src/test/resources/security/mvel-payload-1.json
new file mode 100644
index 0000000..da2c16d
--- /dev/null
+++ b/itests/src/test/resources/security/mvel-payload-1.json
@@ -0,0 +1,20 @@
+{
+ "filters": [
+ {
+ "id": "filter1",
+ "filters": [
+ {
+ "condition": {
+ "parameterValues": {
+ "propertyName": "prop",
+ "comparisonOperator": "equals",
+ "propertyValue": "script::Runtime r = Runtime.getRuntime(); r.exec(\"touch ###VULN_FILE_PATH###\");"
+ },
+ "type": "profilePropertyCondition"
+ }
+ }
+ ]
+ }
+ ],
+ "sessionId": "demo-session-id"
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/security/ognl-payload-1.json b/itests/src/test/resources/security/ognl-payload-1.json
new file mode 100644
index 0000000..526a914
--- /dev/null
+++ b/itests/src/test/resources/security/ognl-payload-1.json
@@ -0,0 +1,28 @@
+{
+ "personalizations":[
+ {
+ "id":"gender-test",
+ "strategy":"matching-first",
+ "strategyOptions":{
+ "fallback":"var2"
+ },
+ "contents":[
+ {
+ "filters":[
+ {
+ "condition":{
+ "parameterValues":{
+ "propertyName":"(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).(#getruntimemethod = #runtimeclass.getDeclaredMethods().{^ #this.name.equals(\"getRuntime\")}[0]).(#rtobj = #getruntimemethod.invoke(null,null)).(#execmethod = #runtimeclass.getDeclaredMethods().{? #this.name.equals(\"exec\")}.{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.{? #this.getParameters().length < 2}[0]).(#execmethod.invoke(#rtobj,\" touch ###VULN_FI [...]
+ "comparisonOperator":"equals",
+ "propertyValue":"male"
+ },
+ "type":"profilePropertyCondition"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "sessionId":"demo-session-id"
+}
diff --git a/kar/pom.xml b/kar/pom.xml
index e86b0b6..06b0641 100644
--- a/kar/pom.xml
+++ b/kar/pom.xml
@@ -149,6 +149,11 @@
<artifactId>unomi-web-tracker-wab</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-scripting</artifactId>
+ <version>${project.version}</version>
+ </dependency>
</dependencies>
<build>
diff --git a/kar/src/main/feature/feature.xml b/kar/src/main/feature/feature.xml
index 75a0efb..6444903 100644
--- a/kar/src/main/feature/feature.xml
+++ b/kar/src/main/feature/feature.xml
@@ -56,6 +56,7 @@
<bundle start-level="70" start="false">mvn:org.apache.unomi/unomi-lifecycle-watcher/${project.version}</bundle>
<bundle start-level="75" start="false">mvn:org.apache.unomi/unomi-api/${project.version}</bundle>
<bundle start-level="75" start="false">mvn:org.apache.unomi/unomi-common/${project.version}</bundle>
+ <bundle start-level="75" start="false">mvn:org.apache.unomi/unomi-scripting/${project.version}</bundle>
<bundle start-level="75" start="false">mvn:org.apache.unomi/unomi-metrics/${project.version}</bundle>
<bundle start-level="75" start="false">mvn:org.apache.unomi/unomi-persistence-spi/${project.version}</bundle>
<bundle start-level="76" start="false">mvn:org.apache.unomi/unomi-persistence-elasticsearch-core/${project.version}</bundle>
diff --git a/package/src/main/resources/etc/custom.system.properties b/package/src/main/resources/etc/custom.system.properties
index 1342313..d0490bf 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -31,8 +31,13 @@ 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.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.util.HashMap,java.lang.Integer,org.mvel2.*}
org.apache.unomi.scripting.forbid=${env:UNOMI_FORBID_SCRIPTING_CLASSES:-}
+org.apache.unomi.scripting.filter.allow=${env:UNOMI_SCRIPTING_FILTER_ALLOW:-all}
+org.apache.unomi.scripting.filter.forbid=${env:UNOMI_SCRIPTING_FILTER_FORBID:-.*Runtime.*,.*ProcessBuilder.*,.*exec.*,.*invoke.*,.*getClass.*,.*Class.*,.*ClassLoader.*,.*System.*,.*Method.*,.*method.*,.*Compiler.*,.*Thread.*,.*FileWriter.*,.*forName.*,.*Socket.*,.*DriverManager.*}
+org.apache.unomi.security.properties.useOGNLScripting=${env:UNOMI_SCRIPTING_USE_OGNL:-false}
+org.apache.unomi.security.personalization.sanitizeConditions=${env:UNOMI_SECURITY_SANITIZEPERSONALIZATIONCONDITIONS:-true}
+
#######################################################################################################################
## HTTP Settings ##
#######################################################################################################################
diff --git a/persistence-elasticsearch/core/pom.xml b/persistence-elasticsearch/core/pom.xml
index b62a426..334a931 100644
--- a/persistence-elasticsearch/core/pom.xml
+++ b/persistence-elasticsearch/core/pom.xml
@@ -133,10 +133,6 @@
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
- <dependency>
- <groupId>org.mvel</groupId>
- <artifactId>mvel2</artifactId>
- </dependency>
<dependency>
<groupId>org.apache.unomi</groupId>
@@ -163,6 +159,13 @@
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-scripting</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
</dependencies>
<build>
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 8355015..13bc667 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,25 +24,21 @@ 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;
+import org.apache.unomi.scripting.ScriptExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.Reader;
-import java.io.Serializable;
import java.io.StringReader;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
public class ConditionContextHelper {
private static final Logger logger = LoggerFactory.getLogger(ConditionContextHelper.class);
- private static Map<String,Serializable> mvelExpressions = new ConcurrentHashMap<>();
-
private static MappingCharFilterFactory mappingCharFilterFactory;
static {
Map<String,String> args = new HashMap<>();
@@ -55,12 +51,12 @@ public class ConditionContextHelper {
}
}
- public static Condition getContextualCondition(Condition condition, Map<String, Object> context) {
+ public static Condition getContextualCondition(Condition condition, Map<String, Object> context, ScriptExecutor scriptExecutor) {
if (!hasContextualParameter(condition.getParameterValues())) {
return condition;
}
@SuppressWarnings("unchecked")
- Map<String, Object> values = (Map<String, Object>) parseParameter(context, condition.getParameterValues());
+ Map<String, Object> values = (Map<String, Object>) parseParameter(context, condition.getParameterValues(), scriptExecutor);
if (values == null) {
return null;
}
@@ -70,7 +66,7 @@ public class ConditionContextHelper {
}
@SuppressWarnings("unchecked")
- private static Object parseParameter(Map<String, Object> context, Object value) {
+ private static Object parseParameter(Map<String, Object> context, Object value, ScriptExecutor scriptExecutor) {
if (value instanceof String) {
if (((String) value).startsWith("parameter::") || ((String) value).startsWith("script::")) {
String s = (String) value;
@@ -78,13 +74,13 @@ public class ConditionContextHelper {
return context.get(StringUtils.substringAfter(s, "parameter::"));
} else if (s.startsWith("script::")) {
String script = StringUtils.substringAfter(s, "script::");
- return executeScript(context, script);
+ return scriptExecutor.execute(script, context);
}
}
} else if (value instanceof Map) {
Map<String, Object> values = new HashMap<String, Object>();
for (Map.Entry<String, Object> entry : ((Map<String, Object>) value).entrySet()) {
- Object parameter = parseParameter(context, entry.getValue());
+ Object parameter = parseParameter(context, entry.getValue(), scriptExecutor);
if (parameter == null) {
return null;
}
@@ -94,7 +90,7 @@ public class ConditionContextHelper {
} else if (value instanceof List) {
List<Object> values = new ArrayList<Object>();
for (Object o : ((List<?>) value)) {
- Object parameter = parseParameter(context, o);
+ Object parameter = parseParameter(context, o, scriptExecutor);
if (parameter != null) {
values.add(parameter);
}
@@ -104,22 +100,6 @@ 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/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionESQueryBuilderDispatcher.java b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionESQueryBuilderDispatcher.java
index d08e283..56b280e 100644
--- a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionESQueryBuilderDispatcher.java
+++ b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionESQueryBuilderDispatcher.java
@@ -18,6 +18,7 @@
package org.apache.unomi.persistence.elasticsearch.conditions;
import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.scripting.ScriptExecutor;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.slf4j.Logger;
@@ -31,10 +32,15 @@ public class ConditionESQueryBuilderDispatcher {
private static final Logger logger = LoggerFactory.getLogger(ConditionESQueryBuilderDispatcher.class.getName());
private Map<String, ConditionESQueryBuilder> queryBuilders = new ConcurrentHashMap<>();
+ private ScriptExecutor scriptExecutor;
public ConditionESQueryBuilderDispatcher() {
}
+ public void setScriptExecutor(ScriptExecutor scriptExecutor) {
+ this.scriptExecutor = scriptExecutor;
+ }
+
public void addQueryBuilder(String name, ConditionESQueryBuilder evaluator) {
queryBuilders.put(name, evaluator);
}
@@ -73,7 +79,7 @@ public class ConditionESQueryBuilderDispatcher {
if (queryBuilders.containsKey(queryBuilderKey)) {
ConditionESQueryBuilder queryBuilder = queryBuilders.get(queryBuilderKey);
- Condition contextualCondition = ConditionContextHelper.getContextualCondition(condition, context);
+ Condition contextualCondition = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor);
if (contextualCondition != null) {
return queryBuilder.buildQuery(contextualCondition, context, this);
}
@@ -107,7 +113,7 @@ public class ConditionESQueryBuilderDispatcher {
if (queryBuilders.containsKey(queryBuilderKey)) {
ConditionESQueryBuilder queryBuilder = queryBuilders.get(queryBuilderKey);
- Condition contextualCondition = ConditionContextHelper.getContextualCondition(condition, context);
+ Condition contextualCondition = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor);
if (contextualCondition != null) {
return queryBuilder.count(contextualCondition, context, this);
}
diff --git a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionEvaluatorDispatcher.java b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionEvaluatorDispatcher.java
index 6394300..cd0bb35 100644
--- a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionEvaluatorDispatcher.java
+++ b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/conditions/ConditionEvaluatorDispatcher.java
@@ -21,6 +21,7 @@ import org.apache.unomi.api.Item;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.metrics.MetricAdapter;
import org.apache.unomi.metrics.MetricsService;
+import org.apache.unomi.scripting.ScriptExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,11 +38,16 @@ public class ConditionEvaluatorDispatcher {
private Map<String, ConditionEvaluator> evaluators = new ConcurrentHashMap<>();
private MetricsService metricsService;
+ private ScriptExecutor scriptExecutor;
public void setMetricsService(MetricsService metricsService) {
this.metricsService = metricsService;
}
+ public void setScriptExecutor(ScriptExecutor scriptExecutor) {
+ this.scriptExecutor = scriptExecutor;
+ }
+
public void addEvaluator(String name, ConditionEvaluator evaluator) {
evaluators.put(name, evaluator);
}
@@ -72,7 +78,7 @@ public class ConditionEvaluatorDispatcher {
return new MetricAdapter<Boolean>(metricsService, this.getClass().getName() + ".conditions." + conditionEvaluatorKey) {
@Override
public Boolean execute(Object... args) throws Exception {
- Condition contextualCondition = ConditionContextHelper.getContextualCondition(condition, context);
+ Condition contextualCondition = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor);
if (contextualCondition != null) {
return evaluator.eval(contextualCondition, item, context, dispatcher);
} else {
diff --git a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index a1f13b5..cafab98 100644
--- a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -62,6 +62,7 @@
<reference id="metricsService" interface="org.apache.unomi.metrics.MetricsService" />
<reference id="hazelcastInstance" interface="com.hazelcast.core.HazelcastInstance" />
+ <reference id="scriptExecutor" interface="org.apache.unomi.scripting.ScriptExecutor" />
<service id="elasticSearchPersistenceService" ref="elasticSearchPersistenceServiceImpl">
<interfaces>
@@ -72,11 +73,13 @@
<bean id="conditionESQueryBuilderDispatcher"
class="org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilderDispatcher">
+ <property name="scriptExecutor" ref="scriptExecutor" />
</bean>
<bean id="conditionEvaluatorDispatcherImpl"
class="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher">
<property name="metricsService" ref="metricsService" />
+ <property name="scriptExecutor" ref="scriptExecutor" />
</bean>
<bean id="elasticSearchPersistenceServiceImpl"
diff --git a/plugins/baseplugin/pom.xml b/plugins/baseplugin/pom.xml
index f35fe97..131d251 100644
--- a/plugins/baseplugin/pom.xml
+++ b/plugins/baseplugin/pom.xml
@@ -84,6 +84,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-scripting</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/PastEventConditionESQueryBuilder.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionESQueryBuilder.java
index c8aeaca..e1b8bd9 100644
--- a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionESQueryBuilder.java
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionESQueryBuilder.java
@@ -27,6 +27,7 @@ import org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBui
import org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilderDispatcher;
import org.apache.unomi.persistence.spi.PersistenceService;
import org.apache.unomi.persistence.spi.aggregate.TermsAggregate;
+import org.apache.unomi.scripting.ScriptExecutor;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
@@ -38,6 +39,7 @@ public class PastEventConditionESQueryBuilder implements ConditionESQueryBuilder
private DefinitionsService definitionsService;
private PersistenceService persistenceService;
private SegmentService segmentService;
+ private ScriptExecutor scriptExecutor;
private int maximumIdsQueryCount = 5000;
private int aggregateQueryBucketSize = 5000;
@@ -50,6 +52,10 @@ public class PastEventConditionESQueryBuilder implements ConditionESQueryBuilder
this.persistenceService = persistenceService;
}
+ public void setScriptExecutor(ScriptExecutor scriptExecutor) {
+ this.scriptExecutor = scriptExecutor;
+ }
+
public void setMaximumIdsQueryCount(int maximumIdsQueryCount) {
this.maximumIdsQueryCount = maximumIdsQueryCount;
}
@@ -175,7 +181,7 @@ public class PastEventConditionESQueryBuilder implements ConditionESQueryBuilder
andCondition.setParameter("operator", "and");
andCondition.setParameter("subConditions", l);
- l.add(ConditionContextHelper.getContextualCondition(eventCondition, context));
+ l.add(ConditionContextHelper.getContextualCondition(eventCondition, context, scriptExecutor));
Integer numberOfDays = (Integer) condition.getParameter("numberOfDays");
if (numberOfDays != null) {
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java
index 8d4a6a2..0d4aaa6 100644
--- a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java
@@ -26,6 +26,7 @@ import org.apache.unomi.persistence.elasticsearch.conditions.ConditionContextHel
import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator;
import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher;
import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.scripting.ScriptExecutor;
import java.util.ArrayList;
import java.util.List;
@@ -37,6 +38,8 @@ public class PastEventConditionEvaluator implements ConditionEvaluator {
private DefinitionsService definitionsService;
+ private ScriptExecutor scriptExecutor;
+
public void setPersistenceService(PersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
@@ -45,6 +48,10 @@ public class PastEventConditionEvaluator implements ConditionEvaluator {
this.definitionsService = definitionsService;
}
+ public void setScriptExecutor(ScriptExecutor scriptExecutor) {
+ this.scriptExecutor = scriptExecutor;
+ }
+
@Override
public boolean eval(Condition condition, Item item, Map<String, Object> context, ConditionEvaluatorDispatcher dispatcher) {
@@ -76,7 +83,7 @@ public class PastEventConditionEvaluator implements ConditionEvaluator {
andCondition.setParameter("operator", "and");
andCondition.setParameter("subConditions", l);
- l.add(ConditionContextHelper.getContextualCondition(eventCondition, context));
+ l.add(ConditionContextHelper.getContextualCondition(eventCondition, context, scriptExecutor));
Condition profileCondition = new Condition();
profileCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
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 3b8da80..af73106 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,7 +23,10 @@ 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.api.rules.Rule;
+import org.apache.unomi.scripting.ExpressionFilter;
+import org.apache.unomi.scripting.ExpressionFilterFactory;
+import org.apache.unomi.scripting.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;
@@ -53,11 +56,19 @@ public class PropertyConditionEvaluator implements ConditionEvaluator {
private Map<String, Map<String, ExpressionAccessor>> expressionCache = new HashMap<>(64);
private boolean usePropertyConditionOptimizations = true;
+ private static ClassLoader secureFilteringClassLoader = new SecureFilteringClassLoader(PropertyConditionEvaluator.class.getClassLoader());
+ private ExpressionFilterFactory expressionFilterFactory;
+
+ private boolean useOGNLScripting = Boolean.parseBoolean(System.getProperty("org.apache.unomi.security.properties.useOGNLScripting", "false"));
public void setUsePropertyConditionOptimizations(boolean usePropertyConditionOptimizations) {
this.usePropertyConditionOptimizations = usePropertyConditionOptimizations;
}
+ public void setExpressionFilterFactory(ExpressionFilterFactory expressionFilterFactory) {
+ this.expressionFilterFactory = expressionFilterFactory;
+ }
+
private int compare(Object actualValue, String expectedValue, Object expectedValueDate, Object expectedValueInteger, Object expectedValueDateExpr) {
if (expectedValue == null && expectedValueDate == null && expectedValueInteger == null && getDate(expectedValueDateExpr) == null) {
return actualValue == null ? 0 : 1;
@@ -256,7 +267,12 @@ public class PropertyConditionEvaluator implements ConditionEvaluator {
return result;
}
}
- return getOGNLPropertyValue(item, expression);
+ if (useOGNLScripting) {
+ return getOGNLPropertyValue(item, expression);
+ } else {
+ logger.warn("OGNL Off. Expression not evaluated : {}", expression);
+ return null;
+ }
}
protected Object getHardcodedPropertyValue(Item item, String expression) {
@@ -271,10 +287,16 @@ public class PropertyConditionEvaluator implements ConditionEvaluator {
return event.getTarget().getItemId();
}
if (expression.startsWith("target.properties.")) {
- if (event.getTarget() instanceof CustomItem) {
- CustomItem customItem = (CustomItem) event.getTarget();
- String expressionPart = expression.substring("target.properties.".length());
- return getNestedPropertyValue(expressionPart, customItem.getProperties());
+ String expressionPart = expression.substring("target.properties.".length());
+ Item targetItem = event.getTarget();
+ if (targetItem instanceof CustomItem) {
+ return getNestedPropertyValue(expressionPart, ((CustomItem) targetItem).getProperties());
+ } else if (targetItem instanceof Session) {
+ return getNestedPropertyValue(expressionPart, ((Session) targetItem).getProperties());
+ } else if (targetItem instanceof Rule) {
+ return null;
+ } else if (targetItem instanceof Profile) {
+ return getNestedPropertyValue(expressionPart, ((Profile) targetItem).getProperties());
}
}
if ("target.scope".equals(expression)) {
@@ -283,8 +305,41 @@ public class PropertyConditionEvaluator implements ConditionEvaluator {
if ("scope".equals(expression)) {
return event.getScope();
}
+ if ("eventType".equals(expression)) {
+ return event.getEventType();
+ }
+ if ("profile".equals(expression)) {
+ return event.getProfile();
+ }
+ if ("profileId".equals(expression)) {
+ return event.getProfileId();
+ }
+ if ("session".equals(expression)) {
+ return event.getSession();
+ }
+ if ("sessionId".equals(expression)) {
+ return event.getSessionId();
+ }
+ if ("source".equals(expression)) {
+ return event.getSource();
+ }
+ if ("target".equals(expression)) {
+ return event.getTarget();
+ }
+ if ("timeStamp".equals(expression)) {
+ return event.getTimeStamp();
+ }
+ if ("itemId".equals(expression)) {
+ return event.getItemId();
+ }
+ if ("itemType".equals(expression)) {
+ return event.getItemType();
+ }
} else if (item instanceof Session) {
Session session = (Session) item;
+ if ("scope".equals(expression)) {
+ return session.getScope();
+ }
if ("timeStamp".equals(expression)) {
return session.getTimeStamp();
}
@@ -294,12 +349,27 @@ public class PropertyConditionEvaluator implements ConditionEvaluator {
if ("size".equals(expression)) {
return session.getSize();
}
+ if ("lastEventDate".equals(expression)) {
+ return session.getLastEventDate();
+ }
if (expression.startsWith("properties.")) {
return getNestedPropertyValue(expression.substring("properties.".length()), session.getProperties());
}
if (expression.startsWith("systemProperties.")) {
return getNestedPropertyValue(expression.substring("systemProperties.".length()), session.getSystemProperties());
}
+ if ("itemId".equals(expression)) {
+ return session.getItemId();
+ }
+ if ("itemType".equals(expression)) {
+ return session.getItemType();
+ }
+ if ("profile".equals(expression)) {
+ return session.getProfile();
+ }
+ if ("profileId".equals(expression)) {
+ return session.getProfileId();
+ }
} else if (item instanceof Profile) {
Profile profile = (Profile) item;
if ("segments".equals(expression)) {
@@ -317,17 +387,38 @@ public class PropertyConditionEvaluator implements ConditionEvaluator {
if (expression.startsWith("systemProperties.")) {
return getNestedPropertyValue(expression.substring("systemProperties.".length()), profile.getSystemProperties());
}
+ if ("itemId".equals(expression)) {
+ return profile.getItemId();
+ }
+ if ("itemType".equals(expression)) {
+ return profile.getItemType();
+ }
+ if ("mergedWith".equals(expression)) {
+ return profile.getMergedWith();
+ }
} else if (item instanceof CustomItem) {
CustomItem customItem = (CustomItem) item;
if (expression.startsWith("properties.")) {
return getNestedPropertyValue(expression.substring("properties.".length()), customItem.getProperties());
}
+ if ("itemId".equals(expression)) {
+ return customItem.getItemId();
+ }
+ if ("itemType".equals(expression)) {
+ return customItem.getItemType();
+ }
+ if ("scope".equals(expression)) {
+ return customItem.getScope();
+ }
}
return NOT_OPTIMIZED_MARKER;
}
protected Object getOGNLPropertyValue(Item item, String expression) throws Exception {
- ClassLoader secureFilteringClassLoader = new SecureFilteringClassLoader(PropertyConditionEvaluator.class.getClassLoader());
+ if (expressionFilterFactory.getExpressionFilter("ognl").filter(expression) == null) {
+ logger.warn("Expression {} is not allowed !", expression);
+ return null;
+ }
OgnlContext ognlContext = getOgnlContext(secureFilteringClassLoader);
ExpressionAccessor accessor = getPropertyAccessor(item, expression, ognlContext, secureFilteringClassLoader);
return accessor != null ? accessor.get(ognlContext, item) : null;
@@ -375,8 +466,15 @@ public class PropertyConditionEvaluator implements ConditionEvaluator {
@Override
public boolean isAccessible(Map context, Object target, Member member, String propertyName) {
int modifiers = member.getModifiers();
- boolean result = Modifier.isPublic(modifiers);
- return result;
+ if (target instanceof Item) {
+ if ("getClass".equals(member.getName())) {
+ logger.warn("Target {} and member {} for property {} are not allowed by OGNL security filter", target, member, propertyName);
+ return false;
+ }
+ return Modifier.isPublic(modifiers);
+ }
+ logger.warn("Target {} and member {} for property {} are not allowed by OGNL security filter", target, member, propertyName);
+ return false;
}
}, new ClassLoaderClassResolver(classLoader),
null);
diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/expressions/mvel.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/expressions/mvel.json
new file mode 100644
index 0000000..6c0a5a0
--- /dev/null
+++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/expressions/mvel.json
@@ -0,0 +1,13 @@
+[
+ "\\Q'systemProperties.goals.'+goalId+'TargetReached'\\E",
+ "\\Q'now-'+since+'d'\\E",
+ "\\Q'scores.'+scoringPlanId\\E",
+ "\\QminimumDuration*1000\\E",
+ "\\QmaximumDuration*1000\\E",
+ "\\Qprofile.properties.?nbOfVisits != null ? (profile.properties.nbOfVisits + 1) : 1\\E",
+ "\\Qsession != null ? session.size + 1 : 0\\E",
+ "\\Q'properties.optimizationTest_'+event.target.itemId\\E",
+ "\\Qevent.target.properties.variantId\\E",
+ "\\Qprofile.properties.?systemProperties.goals.\\E[\\w\\_]*\\QReached != null ? (profile.properties.systemProperties.goals.\\E[\\w\\_]*\\QReached) : 'now'\\E",
+ "\\Qprofile.properties.?systemProperties.campaigns.\\E[\\w\\_]*\\QEngaged != null ? (profile.properties.systemProperties.campaigns.\\E[\\w\\_]*\\QEngaged) : 'now'\\E"
+]
\ No newline at end of file
diff --git a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 5a10214..cd75ce1 100644
--- a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -44,6 +44,7 @@
<reference id="segmentService" interface="org.apache.unomi.api.services.SegmentService"/>
<reference id="eventService" interface="org.apache.unomi.api.services.EventService"/>
<reference id="configSharingService" interface="org.apache.unomi.api.services.ConfigSharingService" />
+ <reference id="scriptExecutor" interface="org.apache.unomi.scripting.ScriptExecutor"/>
<service
interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilder">
@@ -102,6 +103,7 @@
<property name="definitionsService" ref="definitionsService"/>
<property name="persistenceService" ref="persistenceService"/>
<property name="segmentService" ref="segmentService"/>
+ <property name="scriptExecutor" ref="scriptExecutor" />
<property name="maximumIdsQueryCount" value="${es.maximumIdsQueryCount}"/>
<property name="aggregateQueryBucketSize" value="${es.aggregateQueryBucketSize}"/>
</bean>
@@ -163,6 +165,7 @@
<bean class="org.apache.unomi.plugins.baseplugin.conditions.PastEventConditionEvaluator">
<property name="definitionsService" ref="definitionsService"/>
<property name="persistenceService" ref="persistenceService"/>
+ <property name="scriptExecutor" ref="scriptExecutor" />
</bean>
</service>
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 dff90fb..a9bbf88 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
@@ -21,6 +21,9 @@ 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.apache.unomi.scripting.ExpressionFilter;
+import org.apache.unomi.scripting.ExpressionFilterFactory;
+import org.junit.Before;
import org.junit.Test;
import java.io.File;
@@ -29,6 +32,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
+import java.util.regex.Pattern;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNull;
@@ -49,6 +53,27 @@ public class PropertyConditionEvaluatorTest {
private static Profile mockProfile = generateMockProfile();
private static Session mockSession = generateMockSession();
+ @Before
+ public void setup() {
+ propertyConditionEvaluator.setExpressionFilterFactory(new ExpressionFilterFactory() {
+ @Override
+ public ExpressionFilter getExpressionFilter(String filterCollection) {
+ Set<Pattern> allowedExpressions = new HashSet<>();
+ allowedExpressions.add(Pattern.compile("target\\.itemId"));
+ allowedExpressions.add(Pattern.compile("target\\.scope"));
+ allowedExpressions.add(Pattern.compile("target\\.properties\\.pageInfo\\.pagePath"));
+ allowedExpressions.add(Pattern.compile("target\\.properties\\.pageInfo\\.pageURL"));
+ allowedExpressions.add(Pattern.compile("size"));
+ allowedExpressions.add(Pattern.compile("lastEventDate"));
+ allowedExpressions.add(Pattern.compile("systemProperties\\.goals\\._csk6r4cgeStartReached"));
+ allowedExpressions.add(Pattern.compile("properties\\.email"));
+ allowedExpressions.add(Pattern.compile("systemProperties\\.goals\\._csk6r4cgeStartReached"));
+ Set<Pattern> forbiddenExpressions = new HashSet<>();
+ return new ExpressionFilter(allowedExpressions, forbiddenExpressions);
+ }
+ });
+ }
+
@Test
public void testHardcodedEvaluator() {
Event mockEvent = generateMockEvent();
@@ -62,7 +87,7 @@ public class PropertyConditionEvaluatorTest {
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, "profile.itemId"));
}
@@ -136,6 +161,13 @@ public class PropertyConditionEvaluatorTest {
// we ignore these exceptions as they are expected.
}
vulnFile = new File("target/vuln-file.txt");
+ assertFalse("Vulnerability successfully executed ! File created at " + vulnFileCanonicalPath, vulnFile.exists());
+ try {
+ propertyConditionEvaluator.getOGNLPropertyValue(mockEvent, "(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).(#getruntimemethod = #runtimeclass.getDeclaredMethods().{^ #this.name.equals(\"getRuntime\")}[0]).(#rtobj = #getruntimemethod.invoke(null,null)).(#execmethod = #runtimeclass.getDeclaredMethods().{? #this.name.equals(\"exec\")}.{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.{? #this.getParameters().length < 2}[0]).(#execme [...]
+ } 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());
}
diff --git a/pom.xml b/pom.xml
index a67de95..d735f45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -694,7 +694,7 @@
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
- <version>1.9.2</version>
+ <version>1.9.4</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
@@ -944,6 +944,7 @@
<module>api</module>
<module>common</module>
<module>metrics</module>
+ <module>scripting</module>
<module>persistence-spi</module>
<module>lifecycle-watcher</module>
<module>persistence-elasticsearch</module>
diff --git a/plugins/baseplugin/pom.xml b/scripting/pom.xml
similarity index 55%
copy from plugins/baseplugin/pom.xml
copy to scripting/pom.xml
index f35fe97..61c10e6 100644
--- a/plugins/baseplugin/pom.xml
+++ b/scripting/pom.xml
@@ -21,83 +21,52 @@
<parent>
<groupId>org.apache.unomi</groupId>
- <artifactId>unomi-plugins</artifactId>
- <version>1.4.1-SNAPSHOT</version>
+ <artifactId>unomi-root</artifactId>
+ <version>2.0.0-SNAPSHOT</version>
</parent>
- <artifactId>unomi-plugins-base</artifactId>
- <name>Apache Unomi :: Plugins :: Base Actions and Conditions</name>
- <description>Base actions and conditions plugin for the Apache Unomi Context Server</description>
+ <artifactId>unomi-scripting</artifactId>
+ <name>Apache Unomi :: Scripting Services</name>
+ <description>Scripting services used in the Apache Unomi Context server</description>
<packaging>bundle</packaging>
<dependencies>
<dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>commons-collections</groupId>
- <artifactId>commons-collections</artifactId>
- </dependency>
- <dependency>
- <groupId>commons-beanutils</groupId>
- <artifactId>commons-beanutils</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.servicemix.bundles</groupId>
- <artifactId>org.apache.servicemix.bundles.joda-time</artifactId>
- <scope>provided</scope>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
</dependency>
+
<dependency>
- <groupId>org.apache.unomi</groupId>
- <artifactId>unomi-persistence-elasticsearch-core</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
+ <groupId>org.mvel</groupId>
+ <artifactId>mvel2</artifactId>
</dependency>
+
<dependency>
<groupId>org.apache.unomi</groupId>
- <artifactId>unomi-persistence-spi</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
+ <artifactId>unomi-api</artifactId>
+ <version>2.0.0-SNAPSHOT</version>
+ <scope>test</scope>
</dependency>
+
<dependency>
- <groupId>ognl</groupId>
- <artifactId>ognl</artifactId>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>1.6.6</version>
+ <scope>test</scope>
</dependency>
+
<dependency>
- <groupId>javassist</groupId>
- <artifactId>javassist</artifactId>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.core</artifactId>
+ <scope>provided</scope>
</dependency>
+
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<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>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.11</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-simple</artifactId>
- <version>1.6.6</version>
- <scope>test</scope>
- </dependency>
</dependencies>
@@ -111,8 +80,9 @@
<instructions>
<Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
<Import-Package>
- javassist;resolution:=optional,
- !com.sun.jdi*,
+ org.apache.unomi.api,
+ org.apache.unomi.api.conditions,
+ org.apache.unomi.api.goals,
*
</Import-Package>
</instructions>
@@ -120,4 +90,5 @@
</plugin>
</plugins>
</build>
+
</project>
diff --git a/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilter.java b/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilter.java
new file mode 100644
index 0000000..74d4974
--- /dev/null
+++ b/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilter.java
@@ -0,0 +1,59 @@
+/*
+ * 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.scripting;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * An expression filter is used to allow/deny scripts for execution.
+ */
+public class ExpressionFilter {
+ private static final Logger logger = LoggerFactory.getLogger(ExpressionFilter.class.getName());
+
+ private final Set<Pattern> allowedExpressionPatterns;
+ private final Set<Pattern> forbiddenExpressionPatterns;
+
+ public ExpressionFilter(Set<Pattern> allowedExpressionPatterns, Set<Pattern> forbiddenExpressionPatterns) {
+ this.allowedExpressionPatterns = allowedExpressionPatterns;
+ this.forbiddenExpressionPatterns = forbiddenExpressionPatterns;
+ }
+
+ public String filter(String expression) {
+ if (forbiddenExpressionPatterns != null && expressionMatches(expression, forbiddenExpressionPatterns)) {
+ logger.warn("Expression {} is forbidden by expression filter", expression);
+ return null;
+ }
+ if (allowedExpressionPatterns != null && !expressionMatches(expression, allowedExpressionPatterns)) {
+ logger.warn("Expression {} is not allowed by expression filter", expression);
+ return null;
+ }
+ return expression;
+ }
+
+ private boolean expressionMatches(String expression, Set<Pattern> patterns) {
+ for (Pattern pattern : patterns) {
+ if (pattern.matcher(expression).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilterFactory.java b/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilterFactory.java
new file mode 100644
index 0000000..67bcc7a
--- /dev/null
+++ b/scripting/src/main/java/org/apache/unomi/scripting/ExpressionFilterFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.scripting;
+
+/**
+ * A factory interface to generate ExpressionFilter instances from a centralized configuration
+ */
+public interface ExpressionFilterFactory {
+
+ /**
+ * Retrieve an expression filter for the specified filterCollection identifier.
+ * @param filterCollection a filter collection identifier, typically something like "mvel", "ognl"
+ * @return an instance of a ExpressionFilter with the configuration retrieved for the specified collection identifier
+ */
+ ExpressionFilter getExpressionFilter(String filterCollection);
+
+}
diff --git a/scripting/src/main/java/org/apache/unomi/scripting/MvelScriptExecutor.java b/scripting/src/main/java/org/apache/unomi/scripting/MvelScriptExecutor.java
new file mode 100644
index 0000000..c247c14
--- /dev/null
+++ b/scripting/src/main/java/org/apache/unomi/scripting/MvelScriptExecutor.java
@@ -0,0 +1,80 @@
+/*
+ * 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.scripting;
+
+import org.mvel2.MVEL;
+import org.mvel2.ParserConfiguration;
+import org.mvel2.ParserContext;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * MVEL script executor implementation
+ */
+public class MvelScriptExecutor implements ScriptExecutor {
+
+ private final static String INVALID_SCRIPT_MARKER = "--- Invalid Script Marker ---";
+
+ private Map<String, Serializable> mvelExpressions = new ConcurrentHashMap<>();
+ private SecureFilteringClassLoader secureFilteringClassLoader = new SecureFilteringClassLoader(getClass().getClassLoader());
+ private ExpressionFilterFactory expressionFilterFactory;
+
+ public void setExpressionFilterFactory(ExpressionFilterFactory expressionFilterFactory) {
+ this.expressionFilterFactory = expressionFilterFactory;
+ }
+
+ @Override
+ public Object execute(String script, Map<String, Object> context) {
+
+ final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+ try {
+ if (!mvelExpressions.containsKey(script)) {
+
+ if (expressionFilterFactory.getExpressionFilter("mvel").filter(script) == null) {
+ mvelExpressions.put(script, INVALID_SCRIPT_MARKER);
+ } else {
+ Thread.currentThread().setContextClassLoader(secureFilteringClassLoader);
+ ParserConfiguration parserConfiguration = new ParserConfiguration();
+ parserConfiguration.setClassLoader(secureFilteringClassLoader);
+ ParserContext parserContext = new ParserContext(parserConfiguration);
+
+ // override hardcoded Class Literals that are inserted by default in MVEL and that may be a security risk
+ parserContext.addImport("Runtime", String.class);
+ parserContext.addImport("System", String.class);
+ parserContext.addImport("ProcessBuilder", String.class);
+ parserContext.addImport("Class", String.class);
+ parserContext.addImport("ClassLoader", String.class);
+ parserContext.addImport("Thread", String.class);
+ parserContext.addImport("Compiler", String.class);
+ parserContext.addImport("ThreadLocal", String.class);
+ parserContext.addImport("SecurityManager", String.class);
+
+ mvelExpressions.put(script, MVEL.compileExpression(script, parserContext));
+ }
+ }
+ if (mvelExpressions.containsKey(script) && mvelExpressions.get(script) != INVALID_SCRIPT_MARKER) {
+ return MVEL.executeExpression(mvelExpressions.get(script), context);
+ } else {
+ return script;
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(tccl);
+ }
+ }
+}
diff --git a/scripting/src/main/java/org/apache/unomi/scripting/ScriptExecutor.java b/scripting/src/main/java/org/apache/unomi/scripting/ScriptExecutor.java
new file mode 100644
index 0000000..1e8ace2
--- /dev/null
+++ b/scripting/src/main/java/org/apache/unomi/scripting/ScriptExecutor.java
@@ -0,0 +1,28 @@
+/*
+ * 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.scripting;
+
+import java.util.Map;
+
+/**
+ * An interface to execute scripts with different implementations.
+ */
+public interface ScriptExecutor {
+
+ Object execute(String script, Map<String,Object> context);
+
+}
diff --git a/common/src/main/java/org/apache/unomi/common/SecureFilteringClassLoader.java b/scripting/src/main/java/org/apache/unomi/scripting/SecureFilteringClassLoader.java
similarity index 85%
rename from common/src/main/java/org/apache/unomi/common/SecureFilteringClassLoader.java
rename to scripting/src/main/java/org/apache/unomi/scripting/SecureFilteringClassLoader.java
index 61b91bf..028d637 100644
--- a/common/src/main/java/org/apache/unomi/common/SecureFilteringClassLoader.java
+++ b/scripting/src/main/java/org/apache/unomi/scripting/SecureFilteringClassLoader.java
@@ -14,14 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.common;
+package org.apache.unomi.scripting;
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
+ * A class loader that uses a allow list and a deny 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 {
@@ -34,7 +34,7 @@ public class SecureFilteringClassLoader extends ClassLoader {
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.*");
+ "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.util.HashMap,java.lang.Integer,org.mvel2.*");
if (systemAllowedClasses != null) {
if ("all".equals(systemAllowedClasses.trim())) {
defaultAllowedClasses = null;
@@ -98,6 +98,22 @@ public class SecureFilteringClassLoader extends ClassLoader {
return delegate.loadClass(name);
}
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) 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 super.loadClass(name, resolve);
+ }
+
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ return super.findClass(name);
+ }
+
private boolean classNameMatches(Set<String> classesToTest, String className) {
for (String classToTest : classesToTest) {
if (classToTest.endsWith("*")) {
diff --git a/scripting/src/main/java/org/apache/unomi/scripting/internal/ExpressionFilterFactoryImpl.java b/scripting/src/main/java/org/apache/unomi/scripting/internal/ExpressionFilterFactoryImpl.java
new file mode 100644
index 0000000..02cb028
--- /dev/null
+++ b/scripting/src/main/java/org/apache/unomi/scripting/internal/ExpressionFilterFactoryImpl.java
@@ -0,0 +1,190 @@
+/*
+ * 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.scripting.internal;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.scripting.ExpressionFilter;
+import org.apache.unomi.scripting.ExpressionFilterFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.*;
+import java.util.regex.Pattern;
+
+public class ExpressionFilterFactoryImpl implements ExpressionFilterFactory,BundleListener {
+
+ private static final Logger logger = LoggerFactory.getLogger(ExpressionFilterFactoryImpl.class.getName());
+
+ private final Map<Bundle,Map<String,Set<Pattern>>> allowedExpressionPatternsByBundle = new HashMap<>();
+
+ private final Map<String,Set<Pattern>> allowedExpressionPatternsByCollection = new HashMap<>();
+ private final Map<String,Set<Pattern>> forbiddenExpressionPatternsByCollection = new HashMap<>();
+
+ private BundleContext bundleContext = null;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private boolean expressionFiltersActivated = Boolean.parseBoolean(System.getProperty("org.apache.unomi.scripting.filter.activated", "true"));
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public ExpressionFilterFactoryImpl() {
+ }
+
+ public void init() {
+ String initialFilterCollections = System.getProperty("org.apache.unomi.scripting.filter.collections", "mvel,ognl");
+ String[] initialFilterCollectionParts = initialFilterCollections.split(",");
+ if (initialFilterCollectionParts != null) {
+ for (String initialFilterCollection : initialFilterCollectionParts) {
+ String systemAllowedPatterns = System.getProperty("org.apache.unomi.scripting.filter."+initialFilterCollection+".allow", null);
+ if (systemAllowedPatterns != null) {
+ Set<Pattern> collectionAllowedExpressionPatterns = new HashSet<>();
+ if (!"all".equals(systemAllowedPatterns.trim())) {
+ collectionAllowedExpressionPatterns = null;
+ } else {
+ if (systemAllowedPatterns.trim().length() > 0) {
+ String[] systemAllowedPatternParts = systemAllowedPatterns.split(",");
+ collectionAllowedExpressionPatterns = new HashSet<>();
+ for (String systemAllowedPatternPart : systemAllowedPatternParts) {
+ collectionAllowedExpressionPatterns.add(Pattern.compile(systemAllowedPatternPart));
+ }
+ }
+ }
+ allowedExpressionPatternsByCollection.put(initialFilterCollection, collectionAllowedExpressionPatterns);
+ }
+
+ String systemForbiddenPatterns = System.getProperty("org.apache.unomi.scripting.filter."+initialFilterCollection+".forbid", ".*Runtime.*,.*ProcessBuilder.*,.*exec.*,.*invoke.*,.*getClass.*,.*Class.*,.*ClassLoader.*,.*System.*,.*Method.*,.*method.*,.*Compiler.*,.*Thread.*,.*FileWriter.*,.*forName.*,.*Socket.*,.*DriverManager.*,eval");
+ if (systemForbiddenPatterns != null) {
+ Set<Pattern> collectionForbiddenExpressionPatterns = new HashSet<>();
+ if (systemForbiddenPatterns.trim().length() > 0) {
+ String[] systemForbiddenPatternParts = systemForbiddenPatterns.split(",");
+ collectionForbiddenExpressionPatterns = new HashSet<>();
+ for (String systemForbiddenPatternPart : systemForbiddenPatternParts) {
+ collectionForbiddenExpressionPatterns.add(Pattern.compile(systemForbiddenPatternPart));
+ }
+ } else {
+ collectionForbiddenExpressionPatterns = null;
+ }
+ forbiddenExpressionPatternsByCollection.put(initialFilterCollection, collectionForbiddenExpressionPatterns);
+ }
+ }
+ }
+
+ if (bundleContext != null) {
+ loadPredefinedAllowedPatterns(bundleContext);
+ for (Bundle bundle : bundleContext.getBundles()) {
+ if (bundle.getBundleContext() != null && bundle.getBundleId() != bundleContext.getBundle().getBundleId()) {
+ loadPredefinedAllowedPatterns(bundle.getBundleContext());
+ }
+ }
+
+ bundleContext.addBundleListener(this);
+ }
+
+ }
+
+ public void destroy() {
+ if (bundleContext != null) {
+ bundleContext.removeBundleListener(this);
+ }
+ }
+
+ public void bundleChanged(BundleEvent event) {
+ switch (event.getType()) {
+ case BundleEvent.STARTED:
+ processBundleStartup(event.getBundle().getBundleContext());
+ break;
+ case BundleEvent.STOPPING:
+ processBundleStop(event.getBundle().getBundleContext());
+ break;
+ }
+ }
+
+ private void processBundleStartup(BundleContext bundleContext) {
+ if (bundleContext == null) {
+ return;
+ }
+ loadPredefinedAllowedPatterns(bundleContext);
+ }
+
+ private void processBundleStop(BundleContext bundleContext) {
+ if (bundleContext == null) {
+ return;
+ }
+ removePredefinedAllowedPatterns(bundleContext);
+ }
+
+ private void loadPredefinedAllowedPatterns(BundleContext bundleContext) {
+ Enumeration<URL> predefinedAllowedExpressions = bundleContext.getBundle().findEntries("META-INF/cxs/expressions", "*.json", true);
+ if (predefinedAllowedExpressions == null) {
+ return;
+ }
+
+ Map<String,Set<Pattern>> predefinedAllowedExpressionsForBundle = new HashMap<>();
+
+ while (predefinedAllowedExpressions.hasMoreElements()) {
+ URL predefinedAllowedExpressionsURL = predefinedAllowedExpressions.nextElement();
+ logger.debug("Found predefined allowed expressions at " + predefinedAllowedExpressionsURL + ", loading... ");
+ try {
+ JsonNode predefinedAllowedExpressionsNode = objectMapper.readTree(predefinedAllowedExpressionsURL);
+ Set<Pattern> bundleAllowedExpressions = new HashSet<>();
+ for (JsonNode predefinedAllowedExpressionNode : predefinedAllowedExpressionsNode) {
+ bundleAllowedExpressions.add(Pattern.compile(predefinedAllowedExpressionNode.asText()));
+ }
+ String collection = predefinedAllowedExpressionsURL.getFile().substring("/META-INF/cxs/expressions/".length(), predefinedAllowedExpressionsURL.getFile().length() - ".json".length());
+ predefinedAllowedExpressionsForBundle.put(collection, bundleAllowedExpressions);
+ Set<Pattern> existingAllowedExpressions = allowedExpressionPatternsByCollection.get(collection);
+ if (existingAllowedExpressions == null) {
+ existingAllowedExpressions = new HashSet<>();
+ }
+ existingAllowedExpressions.addAll(bundleAllowedExpressions);
+ allowedExpressionPatternsByCollection.put(collection, existingAllowedExpressions);
+ } catch (IOException e) {
+ logger.error("Error while loading expressions definition " + predefinedAllowedExpressionsURL, e);
+ }
+ }
+
+ allowedExpressionPatternsByBundle.put(bundleContext.getBundle(), predefinedAllowedExpressionsForBundle);
+ }
+
+ private void removePredefinedAllowedPatterns(BundleContext bundleContext) {
+ Map<String,Set<Pattern>> allowedExpressionPatternsForBundle = allowedExpressionPatternsByBundle.get(bundleContext.getBundle());
+ for (Map.Entry<String,Set<Pattern>> allowedExpressionPatternsEntry : allowedExpressionPatternsForBundle.entrySet()) {
+ Set<Pattern> allowedExpressionPatterns = allowedExpressionPatternsByCollection.get(allowedExpressionPatternsEntry.getKey());
+ allowedExpressionPatterns.removeAll(allowedExpressionPatternsEntry.getValue());
+ allowedExpressionPatternsByCollection.put(allowedExpressionPatternsEntry.getKey(), allowedExpressionPatterns);
+ }
+ }
+
+ @Override
+ public ExpressionFilter getExpressionFilter(String filterCollection) {
+ if (expressionFiltersActivated) {
+ return new ExpressionFilter(allowedExpressionPatternsByCollection.get(filterCollection), forbiddenExpressionPatternsByCollection.get(filterCollection));
+ } else {
+ // if expression filtering is turned off we build an expression filter with no filters and that will accept everything.
+ return new ExpressionFilter(null, null);
+ }
+ }
+}
diff --git a/scripting/src/main/resources/META-INF/cxs/expressions/mvel.json b/scripting/src/main/resources/META-INF/cxs/expressions/mvel.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/scripting/src/main/resources/META-INF/cxs/expressions/mvel.json
@@ -0,0 +1 @@
+[]
diff --git a/scripting/src/main/resources/META-INF/cxs/expressions/ognl.json b/scripting/src/main/resources/META-INF/cxs/expressions/ognl.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/scripting/src/main/resources/META-INF/cxs/expressions/ognl.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/scripting/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/scripting/src/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 0000000..f4a7288
--- /dev/null
+++ b/scripting/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<blueprint xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.1.0"
+ xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+ xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd
+ http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.1.0 http://aries.apache.org/schemas/blueprint-cm/blueprint-cm-1.1.0.xsd">
+
+ <bean id="expressionFilterFactoryImpl" init-method="init" destroy-method="destroy" class="org.apache.unomi.scripting.internal.ExpressionFilterFactoryImpl">
+ <property name="bundleContext" ref="blueprintBundleContext"/>
+ </bean>
+
+ <service id="expressionFilterFactory" ref="expressionFilterFactoryImpl">
+ <interfaces>
+ <value>org.apache.unomi.scripting.ExpressionFilterFactory</value>
+ </interfaces>
+ </service>
+
+ <bean id="mvelScriptExecutor" class="org.apache.unomi.scripting.MvelScriptExecutor">
+ <property name="expressionFilterFactory" ref="expressionFilterFactoryImpl" />
+ </bean>
+
+ <service id="scriptExecutor" ref="mvelScriptExecutor">
+ <interfaces>
+ <value>org.apache.unomi.scripting.ScriptExecutor</value>
+ </interfaces>
+ <service-properties>
+ <entry key="language" value="mvel" />
+ </service-properties>
+ </service>
+
+</blueprint>
diff --git a/scripting/src/test/java/org/apache/unomi/scripting/MvelScriptExecutorTest.java b/scripting/src/test/java/org/apache/unomi/scripting/MvelScriptExecutorTest.java
new file mode 100644
index 0000000..d843c43
--- /dev/null
+++ b/scripting/src/test/java/org/apache/unomi/scripting/MvelScriptExecutorTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.scripting;
+
+import org.apache.unomi.api.CustomItem;
+import org.apache.unomi.api.Event;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertFalse;
+
+public class MvelScriptExecutorTest {
+
+ MvelScriptExecutor scriptExecutor = new MvelScriptExecutor();
+
+ 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";
+
+ @Before
+ public void setup() {
+ scriptExecutor.setExpressionFilterFactory(new ExpressionFilterFactory() {
+ @Override
+ public ExpressionFilter getExpressionFilter(String filterCollection) {
+ Set<Pattern> allowedExpressions = new HashSet<>();
+ Set<Pattern> forbiddenExpressions = new HashSet<>();
+ return new ExpressionFilter(allowedExpressions, forbiddenExpressions);
+ }
+ });
+ }
+
+ @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 = scriptExecutor.execute("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 (Throwable t) {
+ // this is expected since access to these classes should not be allowed
+ System.out.println("Expected error : " + t.getMessage());
+ }
+ System.out.println("result=" + result);
+ try {
+ result = scriptExecutor.execute("import java.util.*;\nimport java.io.*;\nPrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(\"" + vulnFile.getCanonicalPath() + "\", true)));\nwriter.println(\"test\");\nwriter.close();", ctx);
+ } catch (Throwable t) {
+ // this is expected since access to these classes should not be allowed
+ System.out.println("Expected error : " + t.getMessage());
+ }
+ System.out.println("result=" + result);
+ try {
+ result = scriptExecutor.execute("import java.util.*;\nimport java.io.*;\nnew Scanner(new File(\"" + vulnFile.getCanonicalPath() + "\")).useDelimiter(\"\\\\Z\").next();", ctx);
+ } catch (Throwable t) {
+ // this is expected since access to these classes should not be allowed
+ System.out.println("Expected error : " + t.getMessage());
+ }
+ System.out.println("result=" + result);
+ try {
+ result = scriptExecutor.execute("Runtime r = Runtime.getRuntime(); r.exec(\"touch "+vulnFile.getCanonicalPath()+"\");", ctx);
+ } catch (Throwable t) {
+ // this is expected since access to these classes should not be allowed
+ System.out.println("Expected error : " + t.getMessage());
+ }
+ System.out.println("result=" + result);
+ try {
+ result = scriptExecutor.execute("Runtime r = Runtime.getClass().forName(\"java.lang.Runtime\").getDeclaredMethod(\"getRuntime\", null ).invoke(null, null); r.exec(\"touch "+vulnFile.getCanonicalPath()+"\");", ctx);
+ } catch (Throwable t) {
+ // this is expected since access to these classes should not be allowed
+ System.out.println("Expected error : " + t.getMessage());
+ }
+ System.out.println("result=" + result);
+
+ try {
+ ctx.put("goalId", "d; " +
+ "Runtime r = Runtime.getClass().forName(\"java.lang.Runtime\").getDeclaredMethod(\"getRuntime\", null ).invoke(null, null); r.exec(\"touch " +
+ vulnFile.getCanonicalPath() +
+ "\")" +
+ " ; ");
+ result = scriptExecutor.execute("'systemProperties\\.goals\\.'+goalId+'TargetReached'", ctx);
+ } catch (Throwable t) {
+ // this is expected since access to these classes should not be allowed
+ System.out.println("Expected error : " + t.getMessage());
+ }
+ System.out.println("result=" + result);
+ assertFalse("Vulnerability successfully executed ! File created at " + vulnFile.getCanonicalPath(), vulnFile.exists());
+ }
+
+ 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;
+ }
+}
diff --git a/services/pom.xml b/services/pom.xml
index 87b46d2..69dc028 100644
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -75,11 +75,6 @@
</dependency>
<dependency>
- <groupId>org.mvel</groupId>
- <artifactId>mvel2</artifactId>
- </dependency>
-
- <dependency>
<groupId>com.github.fge</groupId>
<artifactId>json-patch</artifactId>
<version>1.9</version>
@@ -160,6 +155,13 @@
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-scripting</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
</dependencies>
<build>
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 0a0d951..e804db2 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,16 +23,14 @@ 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.scripting.ScriptExecutor;
import org.apache.unomi.metrics.MetricAdapter;
import org.apache.unomi.metrics.MetricsService;
-import org.mvel2.MVEL;
-import org.mvel2.ParserConfiguration;
-import org.mvel2.ParserContext;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
@@ -41,15 +39,24 @@ import java.util.concurrent.ConcurrentHashMap;
public class ActionExecutorDispatcher {
private static final Logger logger = LoggerFactory.getLogger(ActionExecutorDispatcher.class.getName());
private static final String VALUE_NAME_SEPARATOR = "::";
- private final Map<String, Serializable> mvelExpressions = new ConcurrentHashMap<>();
private final Map<String, ValueExtractor> valueExtractors = new HashMap<>(11);
private Map<String, ActionExecutor> executors = new ConcurrentHashMap<>();
private MetricsService metricsService;
+ private BundleContext bundleContext;
+ private ScriptExecutor scriptExecutor;
public void setMetricsService(MetricsService metricsService) {
this.metricsService = metricsService;
}
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public void setScriptExecutor(ScriptExecutor scriptExecutor) {
+ this.scriptExecutor = scriptExecutor;
+ }
+
public ActionExecutorDispatcher() {
valueExtractors.put("profileProperty", new ValueExtractor() {
@Override
@@ -189,24 +196,12 @@ 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);
- }
+ protected Object executeScript(String script, Event event) {
+ Map<String, Object> context = new HashMap<>();
+ context.put("event", event);
+ context.put("session", event.getSession());
+ context.put("profile", event.getProfile());
+ return scriptExecutor.execute(script, context);
}
}
diff --git a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index e09a38b..b7e455e 100644
--- a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -64,7 +64,8 @@
<reference id="karafCellarEventProducer" interface="org.apache.karaf.cellar.core.event.EventProducer" />
<reference id="karafCellarGroupManager" interface="org.apache.karaf.cellar.core.GroupManager" />
<reference id="osgiConfigurationAdmin" interface="org.osgi.service.cm.ConfigurationAdmin"/>
- <reference id="metricsService" interface="org.apache.unomi.metrics.MetricsService" />
+ <reference id="metricsService" interface="org.apache.unomi.metrics.MetricsService"/>
+ <reference id="scriptExecutor" interface="org.apache.unomi.scripting.ScriptExecutor" />
<!-- Service definitions -->
@@ -136,6 +137,8 @@
<bean id="actionExecutorDispatcherImpl"
class="org.apache.unomi.services.actions.ActionExecutorDispatcher">
<property name="metricsService" ref="metricsService" />
+ <property name="scriptExecutor" ref="scriptExecutor"/>
+ <property name="bundleContext" ref="blueprintBundleContext"/>
</bean>
<bean id="rulesServiceImpl" class="org.apache.unomi.services.services.RulesServiceImpl"
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
deleted file mode 100644
index 813c067..0000000
--- a/services/src/test/java/org/apache/unomi/services/actions/ActionExecutorDispatcherTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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;
- }
-
-}
diff --git a/tools/shell-commands/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/tools/shell-commands/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 3b27913..7056cf0 100644
--- a/tools/shell-commands/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/tools/shell-commands/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -31,6 +31,7 @@
<value>org.apache.unomi.lifecycle-watcher</value>
<value>org.apache.unomi.api</value>
<value>org.apache.unomi.common</value>
+ <value>org.apache.unomi.scripting</value>
<value>org.apache.unomi.metrics</value>
<value>org.apache.unomi.persistence-spi</value>
<value>org.apache.unomi.persistence-elasticsearch-core</value>
diff --git a/wab/src/main/java/org/apache/unomi/web/ContextServlet.java b/wab/src/main/java/org/apache/unomi/web/ContextServlet.java
index d4bc05c..01cbe95 100644
--- a/wab/src/main/java/org/apache/unomi/web/ContextServlet.java
+++ b/wab/src/main/java/org/apache/unomi/web/ContextServlet.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.unomi.api.*;
+import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.services.*;
import org.apache.unomi.persistence.spi.CustomObjectMapper;
import org.slf4j.Logger;
@@ -35,7 +36,6 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
-import java.io.InputStream;
import java.io.Writer;
import java.util.*;
@@ -59,6 +59,8 @@ public class ContextServlet extends HttpServlet {
private PersonalizationService personalizationService;
private ConfigSharingService configSharingService;
+ private boolean sanitizeConditions = Boolean.parseBoolean(System.getProperty("org.apache.unomi.security.personalization.sanitizeConditions", "true"));
+
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
@@ -350,7 +352,7 @@ public class ContextServlet extends HttpServlet {
List<PersonalizationService.PersonalizedContent> filterNodes = contextRequest.getFilters();
if (filterNodes != null) {
data.setFilteringResults(new HashMap<>());
- for (PersonalizationService.PersonalizedContent personalizedContent : filterNodes) {
+ for (PersonalizationService.PersonalizedContent personalizedContent : sanitizePersonalizedContentObjects(filterNodes)) {
data.getFilteringResults().put(personalizedContent.getId(), personalizationService.filter(profile,
session, personalizedContent));
}
@@ -359,7 +361,7 @@ public class ContextServlet extends HttpServlet {
List<PersonalizationService.PersonalizationRequest> personalizations = contextRequest.getPersonalizations();
if (personalizations != null) {
data.setPersonalizations(new HashMap<>());
- for (PersonalizationService.PersonalizationRequest personalization : personalizations) {
+ for (PersonalizationService.PersonalizationRequest personalization : sanitizePersonalizations(personalizations)) {
data.getPersonalizations().put(personalization.getId(), personalizationService.personalizeList(profile,
session, personalization));
}
@@ -457,4 +459,89 @@ public class ContextServlet extends HttpServlet {
public void setConfigSharingService(ConfigSharingService configSharingService) {
this.configSharingService = configSharingService;
}
+
+ private List<PersonalizationService.PersonalizedContent> sanitizePersonalizedContentObjects(List<PersonalizationService.PersonalizedContent> personalizedContentObjects) {
+ if (!sanitizeConditions) {
+ return personalizedContentObjects;
+ }
+ List<PersonalizationService.PersonalizedContent> result = new ArrayList<>();
+ for (PersonalizationService.PersonalizedContent personalizedContentObject : personalizedContentObjects) {
+ boolean foundInvalidCondition = false;
+ if (personalizedContentObject.getFilters() != null) {
+ for (PersonalizationService.Filter filter : personalizedContentObject.getFilters()) {
+ if (sanitizeCondition(filter.getCondition()) == null) {
+ foundInvalidCondition = true;
+ break;
+ }
+ }
+ }
+ if (!foundInvalidCondition) {
+ result.add(personalizedContentObject);
+ }
+ }
+
+ return result;
+ }
+
+ private List<PersonalizationService.PersonalizationRequest> sanitizePersonalizations(List<PersonalizationService.PersonalizationRequest> personalizations) {
+ if (!sanitizeConditions) {
+ return personalizations;
+ }
+ List<PersonalizationService.PersonalizationRequest> result = new ArrayList<>();
+ for (PersonalizationService.PersonalizationRequest personalizationRequest : personalizations) {
+ List<PersonalizationService.PersonalizedContent> personalizedContents = sanitizePersonalizedContentObjects(personalizationRequest.getContents());
+ if (personalizedContents != null && personalizedContents.size() > 0) {
+ result.add(personalizationRequest);
+ }
+ }
+ return result;
+ }
+
+ private Condition sanitizeCondition(Condition condition) {
+ Map<String,Object> newParameterValues = new LinkedHashMap<>();
+ for (Map.Entry<String,Object> parameterEntry : condition.getParameterValues().entrySet()) {
+ Object sanitizedValue = sanitizeValue(parameterEntry.getValue());
+ if (sanitizedValue != null) {
+ newParameterValues.put(parameterEntry.getKey(), parameterEntry.getValue());
+ } else {
+ return null;
+ }
+ }
+ return condition;
+ }
+
+ private Object sanitizeValue(Object value) {
+ if (value instanceof String) {
+ String stringValue = (String) value;
+ if (stringValue.startsWith("script::") || stringValue.startsWith("parameter::")) {
+ logger.warn("Scripting detected in context request with value {}, filtering out...", value);
+ return null;
+ } else {
+ return stringValue;
+ }
+ } else if (value instanceof List) {
+ List values = (List) value;
+ List newValues = new ArrayList();
+ for (Object listObject : values) {
+ Object newObject = sanitizeValue(listObject);
+ if (newObject != null) {
+ newValues.add(newObject);
+ }
+ }
+ return values;
+ } else if (value instanceof Map) {
+ Map<Object,Object> newMap = new LinkedHashMap<>();
+ ((Map<?, ?>) value).forEach((key, value1) -> {
+ Object newObject = sanitizeValue(value1);
+ if (newObject != null) {
+ newMap.put(key, newObject);
+ }
+ });
+ return newMap;
+ } else if (value instanceof Condition) {
+ return sanitizeCondition((Condition) value);
+ } else {
+ return value;
+ }
+ }
}